报名工具小程序初始代码

This commit is contained in:
liupopo
2024-02-06 09:36:25 +08:00
commit d7420944ba
202 changed files with 41300 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
<template>
<view style="display: flex;flex-direction: row;align-items: center;height:100rpx;">
<input v-if="(inputType=='text')" type="text"
style="margin-right: 30rpx;flex: 1;padding-left: 36rpx;font-size: 30rpx;" :placeholder="hint"
@input="textChange" :value="value" :maxlength="maxlength"></input>
<input v-if="(inputType=='number')" type="number"
style="margin-right: 30rpx;flex: 1;padding-left: 36rpx;font-size: 30rpx;" :placeholder="hint"
@input="textChange" :value="value" :maxlength="maxlength"></input>
<input v-if="(inputType=='idcard')" type="idcard"
style="margin-right: 30rpx;flex: 1;padding-left: 36rpx;font-size: 30rpx;" :placeholder="hint"
@input="textChange" :value="value" :maxlength="maxlength"></input>
<input v-if="(inputType=='digit')" type="digit"
style="margin-right: 30rpx;flex: 1;padding-left: 36rpx;font-size: 30rpx;" :placeholder="hint"
@input="textChange" :value="value" :maxlength="maxlength"></input>
<image v-if="isShowDelete" style="width: 50rpx;height: 50rpx;margin-right: 20rpx;flex-shrink: 0;"
src="../../static/img/public/shanchu1.png" mode="aspectFill" @click="clickRight()">
</image>
</view>
</template>
<script>
export default {
props: {
hint: {
type: String,
default: ""
},
text: {
type: String,
default: ""
},
inputType: {
type: String,
default: "text"
},
maxlength: {
type: Number,
default: 99999999
},
},
watch: {
text: {
handler(t, oldT) {
this.value = t
this.$emit("onTextChange", this.value)
this.isShowDelete = !this.IsEmpty(this.value)
},
immediate: true,
}
},
data() {
return {
isShowDelete: false,
value: ""
};
},
methods: {
textChange(e) {
this.isShowDelete = !this.IsEmpty(e.detail.value)
this.$emit("onTextChange", e.detail.value)
this.value = e.detail.value
},
clickRight() {
this.value = ""
this.$emit("onTextChange", this.value)
this.isShowDelete = !this.IsEmpty(this.value)
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,51 @@
<template>
<view
style="display: flex;flex-direction: row;box-sizing: border-box;width: 100%;align-items: center;height: 100rpx;padding: 0rpx 15rpx; background-color: #FFFFFF;">
<image style="width: 45rpx;height: 45rpx;" src="../../static/custom-icon/line.png" mode="aspectFill">
</image>
<text style="flex-shrink: 0;font-size: 30rpx; color: #101010;">{{leftText}}</text>
<text
style="flex: 1;padding-left: 30rpx;padding-right: 30rpx;color: #898989 ;font-size: 26rpx;text-align: center;">{{middleText}}</text>
<text v-if="isShowRight" style="flex-shrink: 0;font-size: 26rpx;color: #080808;"
@click="clickRight()">{{rightText}}</text>
<image v-if="isShowRight" style="width: 30rpx;height: 30rpx;margin-right: 20rpx;"
src="../../static/img/public/more.png" mode="aspectFill" @click="clickRight()">
</image>
</view>
</template>
<script>
export default {
props: {
leftText: {
type: String,
default: ""
},
middleText: {
type: String,
default: ""
},
rightText: {
type: String,
default: ""
},
isShowRight: {
type: Boolean,
default: true
}
},
data() {
return {
clickRight() {
this.$emit("rightClick", this.leftText)
}
};
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,585 @@
<template>
<view :style="{'background-color':pageBg}">
<view v-if="!hideTop" :style="{'height':totalHeightPx,'background-color': barColor,'box-sizing': 'border-box','width':'100%',
'z-index':'9900','position':'fixed','top':'--window-top','left':'0'}">
<view :style="{'height':statusBarHeightPx}">
</view>
<view
:style="{'height':navHeightPx,'width':navWidthPx,'display': 'flex','flex-direction':'row','box-sizing': 'border-box','justify-content': 'space-between'}">
<view
:style="{'height':navHeightPx,'display': 'flex','flex-direction':'row','box-sizing': 'border-box'}">
<image :style="{'height':navHeightPx,'width':iconPx,'flex-shrink':0}" mode="aspectFit"
:src="backIcon" @click="clickBack"></image>
<text class="titleStyle"
:style="{'height':navHeightPx,'line-height':navHeightPx,'font-size':'33rpx','color':'#FFFFFF'}">{{text}}</text>
</view>
<view
:style="{'height':navHeightPx,'display': 'flex','flex-direction':'row','box-sizing': 'border-box'}">
<view v-if="useTitleLeftBtn!=0"
:style="{'height':navHeightPx,'display': 'flex','flex-direction':'row','box-sizing': 'border-box'}"
@click="clickLeftBtn">
<image v-if="useTitleLeftBtn == 2 && dropLeftList.length == 0"
:style="{'height':navHeightPx,'width':navHeightPx}" mode="aspectFit"
:src="titleLeftBtnSource"></image>
<text v-if="useTitleLeftBtn == 1 && dropLeftList.length == 0"
:style="{'height':navHeightPx,'line-height':navHeightPx,'font-size':'28rpx','color':'#FFFFFF','padding-left':'20rpx','padding-right':'20rpx'}">{{titleLeftBtnSource}}</text>
<drop-item ref="dropview" :candidates="dropLeftList" :btnType="useTitleLeftBtn"
:btnSource="titleLeftBtnSource" @onselect="selectDrop"></drop-item>
</view>
<view v-if="useTitleRightBtn!=0"
:style="{'height':navHeightPx,'display': 'flex','flex-direction':'row','box-sizing': 'border-box'}"
@click="clickRightBtn">
<image v-if="useTitleRightBtn==2 && dropRightList.length == 0"
:style="{'height':navHeightPx,'width':navHeightPx}" mode="aspectFit"
:src="titleRightBtnSource"></image>
<text v-if="useTitleRightBtn==1 && dropRightList.length == 0"
:style="{'height':navHeightPx,'line-height':navHeightPx,'font-size':'28rpx','color':'#FFFFFF','padding-left':'20rpx','padding-right':'20rpx'}">{{titleRightBtnSource}}</text>
<drop-item ref="dropview" :candidates="dropRightList" :btnType="useTitleRightBtn"
:btnSource="titleRightBtnSource" @onselect="selectDrop"></drop-item>
</view>
</view>
</view>
</view>
<view class="mescroll-body"
:style="{'minHeight':minHeight, 'padding-top': totalHeightPx, 'padding-bottom': padBottom, 'padding-bottom': padBottomConstant, 'padding-bottom': padBottomEnv }"
@touchstart="touchstartEvent" @touchmove="touchmoveEvent" @touchend="touchendEvent"
@touchcancel="touchendEvent">
<view class="mescroll-body-content" :style="{ transform: translateY, transition: transition }">
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> -->
<view v-if="useDownScroll" class="mescroll-downwarp"
:style="{'background-color':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
<view class="downwarp-content">
<image class="downwarp-slogan" src="../../static/img/public/mescroll-slogan.png"
mode="widthFix" />
<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view>
<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view>
</view>
</view>
<sl-filter id="header" ref="slLilter" v-if="menuList.length!=0" :color="titleColor" :themeColor="themeColor"
:menuList.sync="menuList" @result="result" ></sl-filter>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick">
</mescroll-empty>
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="useUpScroll && !isDownLoading" class="mescroll-upwarp"
:style="{'background-color':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-if="upLoadType===1">
<view class="upwarp-progress mescroll-rotate"
:style="{'border-color':mescroll.optUp.textColor}"></view>
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
</view>
</view>
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
</view>
</view>
</template>
<script>
import MeScroll from '../mescroll-uni/mescroll-uni.js';
import MescrollEmpty from '../mescroll-uni/components/mescroll-empty.vue';
import MescrollTop from '../mescroll-uni/components/mescroll-top.vue';
import GlobalOption from '../mescroll-uni/mescroll-uni-option.js';
import RefreshOption from './refresh-option.js';
import slFilter from '@/components/sl-filter/sl-filter.vue';
export default {
components: {
MescrollEmpty,
MescrollTop,
slFilter
},
data() {
return {
mescroll: null, // mescroll实例
downHight: 0, //下拉刷新: 容器高度
downLoadType: 4, // 下拉刷新状态 inOffset1 outOffset2 showLoading3 endDownScroll4
upLoadType: 0, // 上拉加载状态0loading前1loading中2没有更多了
isShowEmpty: false, // 是否显示空布局
isShowToTop: false, // 是否显示回到顶部按钮
windowHeight: 0, // 可使用窗口的高度
statusBarH: 0, // 状态栏高度
statusBarHeight: "",
navHeight: "",
navWidth: "",
totalHeight: "",
statusBarHeightPx: "",
navHeightPx: "",
navWidthPx: "",
totalHeightPx: "",
bg: "#2fa1f0",
totalHeightUpx: "",
backIcon: "",
iconPx: "",
LeftPos: '0px',
themeColor: '#000000',
titleColor: '#fd6d2a',
hideTop: false
};
},
props: {
down: Object, // 下拉刷新的参数配置
up: Object, // 上拉加载的参数配置
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
topbar: Boolean, // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可自动加上状态栏高度的偏移量)
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
text: {
type: String,
default: ""
},
pageBg: {
type: String,
default: "#FFFFFF"
},
barColor: {
type: String,
default: "#2fa1f0"
},
hasBack: {
type: Boolean,
default: true
},
isShareIn: {
type: Boolean,
default: false
},
isInterceptBack: {
type: Boolean,
default: false
},
useUpScroll: {
type: Boolean,
default: true
},
useDownScroll: {
type: Boolean,
default: true
},
useTitleLeftBtn: {
type: String,
// 0不使用 1使用文字 2使用图片
default: "0"
},
useTitleRightBtn: {
type: String,
// 0不使用 1使用文字 2使用图片
default: "0"
},
titleLeftBtnSource: {
type: String,
default: ""
},
titleRightBtnSource: {
type: String,
default: ""
},
dropLeftList: {
type: Array,
default () {
return []
}
},
dropRightList: {
type: Array,
default () {
return []
}
},
dropShow: {
type: Boolean,
default: true
},
menuList: {
type: Array,
default () {
return []
}
}
},
computed: {
// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
minHeight() {
return this.toPx(this.height || '100%') + 'px'
},
// 下拉布局往下偏移的距离 (px)
numTop() {
return this.toPx(this.top) + (this.topbar ? this.statusBarH : 0);
},
padTop() {
return this.numTop + 'px';
},
// 上拉布局往上偏移 (px)
numBottom() {
return this.toPx(this.bottom);
},
padBottom() {
return this.numBottom + 'px';
},
padBottomConstant() {
return this.isSafearea ? 'calc(' + this.padBottom + ' + constant(safe-area-inset-bottom))' : this
.padBottom;
},
padBottomEnv() {
return this.isSafearea ? 'calc(' + this.padBottom + ' + env(safe-area-inset-bottom))' : this.padBottom;
},
// 是否为重置下拉的状态
isDownReset() {
return this.downLoadType === 3 || this.downLoadType === 4;
},
// 过渡
transition() {
return this.isDownReset ? 'transform 300ms' : this.downTransition;
},
translateY() {
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' :
''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
},
// 是否在加载中
isDownLoading() {
return this.downLoadType === 3
},
// 旋转的角度
downRotate() {
return this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
}
},
methods: {
refreshTitle(obj) {
this.$refs.slLilter.setTitle(obj)
},
reset(){
this.$refs.slLilter.resetSelectToDefault(function(e) {
callback(e);
});
},
result(val) {
console.log("val", JSON.stringify(val));
this.$emit('result', JSON.stringify(val))
},
selectDrop(index, data) {
let left = this.arrSame(data, this.dropLeftList);
this.$emit("drop", index, left, (left ? this.dropLeftList[index] : this.dropRightList[index]))
},
clickLeftBtn() {
this.$emit("leftBtn", "左侧按钮")
},
clickRightBtn() {
this.$emit("rightBtn", "右侧按钮")
},
clickBack() {
if (this.hasBack) {
if (getCurrentPages().length > 1) {
if (this.isInterceptBack) {
this.$emit("backClick")
} else {
this.Back()
}
} else {
if (RefreshOption.homePageIsTabBar) {
uni.switchTab({
url: RefreshOption.homePagePath
})
} else {
uni.navigateTo({
url: RefreshOption.homePagePath
})
}
}
}
},
//number,rpx,upx,px,% --> px的数值
toPx(num) {
if (typeof num === 'string') {
if (num.indexOf('px') !== -1) {
if (num.indexOf('rpx') !== -1) {
// "10rpx"
num = num.replace('rpx', '');
} else if (num.indexOf('upx') !== -1) {
// "10upx"
num = num.replace('upx', '');
} else {
// "10px"
return Number(num.replace('px', ''));
}
} else if (num.indexOf('%') !== -1) {
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
let rate = Number(num.replace('%', '')) / 100;
return this.windowHeight * rate;
}
}
return num ? uni.upx2px(Number(num)) : 0;
},
//注册列表touchstart事件,用于下拉刷新
touchstartEvent(e) {
if (this.$props.useDownScroll) {
this.mescroll.touchstartEvent(e);
}
},
//注册列表touchmove事件,用于下拉刷新
touchmoveEvent(e) {
if (this.$props.useDownScroll) {
this.mescroll.touchmoveEvent(e);
}
},
//注册列表touchend事件,用于下拉刷新
touchendEvent(e) {
if (this.$props.useDownScroll) {
this.mescroll.touchendEvent(e);
}
},
// 点击空布局的按钮回调
emptyClick() {
this.$emit('emptyclick', this.mescroll);
},
// 点击回到顶部的按钮回调
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
},
refreshFinished(currentPageListSize) {
if (currentPageListSize == undefined || currentPageListSize == null || currentPageListSize === "") {
this.mescroll.endSuccess();
} else {
this.mescroll.endSuccess(currentPageListSize);
}
},
refreshError() {
this.mescroll.endErr();
},
resetPageOne() {
this.mescroll.resetUpScroll()
// 防止刷新的时候 显示不是第一个
this.toTopClick()
},
closeDrop() {
// 关闭框
if (this.$refs.dropview != undefined)
this.$refs.dropview.closeDropItem()
},
/**
* 验证两个object 是否相同
* @param {Object} obj [需要进行验证的数据1]
* @param {Object} newObj [需要进行验证的数据2]
*/
objSame(obj, newObj) {
let bol = true;
if (Object.keys(obj).length != Object.keys(newObj).length) {
return false;
}
for (let key in obj) {
if (obj[key] instanceof Object) {
bol = this.objSame(obj[key], newObj[key]);
if (!bol) {
break;
}
} else if (obj[key] instanceof Array) {
bol = arrSame(obj[key], newObj[key])
if (!bol) {
break;
}
} else if (obj[key] != newObj[key]) {
bol = false;
break;
}
}
return bol
},
/**
* 验证两个数组是否相同
* @param {Array} arr [需要进行验证的数据1]
* @param {Array} newArr [需要进行验证的数据2]
*/
arrSame(arr, newArr) {
let bol = true;
if (arr.length != newArr.length) {
return false;
}
for (let i = 0, n = arr.length; i < n; i++) {
if (arr[i] instanceof Array) {
bol = arrSame(arr[i], newArr[i])
if (!bol) {
break;
}
} else if (arr[i] instanceof Object) {
bol = this.objSame(arr[i], newArr[i])
if (!bol) {
break;
}
} else if (arr[i] != newArr[i]) {
bol = false;
break;
}
}
return bol;
}
},
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
created() {
this.navWidth = getApp().globalData.navWidth
this.navHeight = getApp().globalData.navHeight
this.totalHeight = getApp().globalData.totalHeight
this.statusBarHeight = getApp().globalData.statusBarHeight
this.navWidthPx = getApp().globalData.navWidthPx
this.navHeightPx = getApp().globalData.navHeightPx
this.totalHeightPx = getApp().globalData.totalHeightPx
this.statusBarHeightPx = getApp().globalData.statusBarHeightPx
this.totalHeightUpx = getApp().globalData.totalHeightUpx
if (this.$props.hasBack) {
this.backIcon = getCurrentPages().length > 1 ? RefreshOption.backIcon : RefreshOption.homeIcon
this.iconPx = this.navHeightPx
} else {
this.backIcon = '../../static/custom-icon/no-back.png'
this.iconPx = this.navHeight / 8 * 3 + 'px'
}
let vm = this;
let diyOption = {
// 下拉刷新的配置
down: {
inOffset(mescroll) {
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
},
outOffset(mescroll) {
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
},
onMoving(mescroll, rate, downHight) {
// 下拉过程中的回调,滑动过程一直在执行;
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
showLoading(mescroll, downHight) {
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
endDownScroll(mescroll) {
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
// 派发下拉刷新的回调
callback: function(mescroll) {
vm.mescroll.resetUpScroll()
}
},
// 上拉加载的配置
up: {
// 显示加载中的回调
showLoading() {
vm.upLoadType = 1;
},
// 显示无更多数据的回调
showNoMore() {
vm.upLoadType = 2;
},
// 隐藏上拉加载的回调
hideUpScroll() {
vm.upLoadType = 0;
},
// 空布局
empty: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowEmpty = isShow;
}
},
// 回到顶部
toTop: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowToTop = isShow;
}
},
// 派发上拉加载的回调
callback: function(mescroll) {
vm.$emit('refresh', mescroll);
}
}
};
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
let myOption = JSON.parse(
JSON.stringify({
down: vm.down,
up: vm.up
})
); // 深拷贝,避免对props的影响
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
// 初始化MeScroll对象
vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
// init回调mescroll对象
vm.$emit('init', vm.mescroll);
// 设置高度
const sys = uni.getSystemInfoSync();
if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
if (sys.statusBarHeight) vm.statusBarH = sys.statusBarHeight;
// 使down的bottomOffset生效
vm.mescroll.setBodyHeight(sys.windowHeight);
// mescroll-body在Android小程序下拉会卡顿,无法像mescroll-uni那样通过设置"disableScroll":true解决,只能用动画过渡缓解
// #ifdef MP
if (sys.platform == "android") vm.downTransition = 'transform 200ms'
// #endif
// 因为使用的是page的scroll,这里需自定义scrollTo
vm.mescroll.resetScrollTo((y, t) => {
uni.pageScrollTo({
scrollTop: y,
duration: t
})
});
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
if (sys.platform == "ios") {
vm.isSafearea = vm.safearea;
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
} else {
vm.isSafearea = false
vm.mescroll.optUp.toTop.safearea = false
}
}
};
</script>
<style lang="scss">
@import "@/components/mescroll-uni/mescroll-body.css";
@import "@/components/mescroll-uni/components/mescroll-down.css";
@import "@/components/mescroll-uni/components/mescroll-up.css";
@import "@/components/RefreshView/mescroll-down.css";
.titleStyle {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,65 @@
/*下拉刷新--标语*/
.mescroll-downwarp .downwarp-slogan {
display: block;
width: 420rpx;
height: 168rpx;
margin: auto;
}
/*下拉刷新--向下进度动画*/
.mescroll-downwarp .downwarp-progress {
display: inline-block;
width: 40rpx;
height: 40rpx;
border: none;
margin: auto;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-image: url(../../static/img/public/mescroll-progress.png);
transition: all 300ms;
}
/*下拉刷新--进度条*/
.mescroll-downwarp .downwarp-loading {
display: inline-block;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #FF8095;
border-bottom-color: transparent;
}
/*下拉刷新--吉祥物*/
.mescroll-downwarp .downwarp-mascot {
position: absolute;
right: 16rpx;
bottom: 0;
width: 100rpx;
height: 100rpx;
background-size: contain;
background-repeat: no-repeat;
animation: animMascot .6s steps(1, end) infinite;
}
@keyframes animMascot {
0% {
background-image: url(http://www.mescroll.com/img/beibei/mescroll-bb1.png)
}
25% {
background-image: url(http://www.mescroll.com/img/beibei/mescroll-bb2.png)
}
50% {
background-image: url(http://www.mescroll.com/img/beibei/mescroll-bb3.png)
}
75% {
background-image: url(http://www.mescroll.com/img/beibei/mescroll-bb4.png)
}
100% {
background-image: url(http://www.mescroll.com/img/beibei/mescroll-bb1.png)
}
}

View File

@@ -0,0 +1,79 @@
***********************************下拉框*************************************
1.dropLeftList: type: Array, default: []
// 左边下拉内容名称list
2.dropRightList: type: Array, default: []
// 右边下拉内容名称list
3.@drop :drop(id,isLeft,selectData)
// 监听用户选择的条目id,是否是左侧按钮的下拉,选中的数据
4.closeDrop()
// 关闭下拉框
***********************************导航栏*************************************
1.hasBack: type: Boolean,default: true
// 是否有返回键
2.useDownScroll: type: Boolean,default: true
// 是否支持下拉刷新
3.useUpScroll: ype: Boolean,default: true
// 是否支持上拉加载
// 使用上拉加载时 需引入
// import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
// mixins: [MescrollMixin]
4.text: type: String,default: ""
// 导航栏标题
5.useTitleLeftBtn: type: String,default: "0"
// 导航栏最右边的两个按钮其中左边的按钮 0不使用 1使用文字 2使用图片
6.useTitleRightBtn: type: String,default: "0"
// 导航栏最右边的两个按钮其中右边的按钮 0不使用 1使用文字 2使用图片
7.titleLeftBtnSource: type: String,default: ""
// 导航栏最右边的两个按钮其中左边的按钮文字或图片资源
8.titleRightBtnSource: type: String,default: ""
// 导航栏最右边的两个按钮其中右边的按钮文字或图片资源
9.@refresh: refresh(page)
// 下拉或上拉的监听 参数为当前请求的页数
10.@leftBtn:
// 导航栏最右边的两个按钮其中左边的按钮点击的监听
11.@rightBtn:
// 导航栏最右边的两个按钮其中右边的按钮点击的监听
12.方法: refreshFinished()
// 网络请求成功后关闭下拉刷新动画与上拉加载动画
// 如果为分页请求 参数为当前接口返回list数据的长度必须
// 否则参数可不传
13.方法: refreshError()
// 网络请求失败后关闭下拉刷新动画与上拉加载动画
14.方法: resetPageOne()
// 调用此方法可重置为第一页(比如说退赛需要重新刷新列表,调用此方法会响应 @refresh 监听)
15.pageBg: type: String,default: ""
// 页面背景色
16.barColor: type: String,default: ""
// 顶部导航栏颜色
17.isShareIn: type: Boolean,default: true
// 是否是分享进入(分享进入返回键会显示主页)
// h5分享时需加入 isShareIn=true 小程序分享时onShareAppMessage() 也需加入isShareIn=true
18.isInterceptBack: type: Boolean,default: false
// 是否拦截返回键
// :isInterceptBack="true" @backClick = "方法名"
// 点击返回时会回调到上面设置方法名的那个方法里

View File

@@ -0,0 +1,19 @@
// 全局配置
const RefreshOption = {
// 背景色
pageBg: '#FFFFFF',
// 导航栏颜色
barColor: '#2fa1f0',
// 是否有返回键(会自动判断是否可返回)
hasBack: true,
// 首页
homePagePath: '../../pages/home/FindFragment',
// 首页是否是切换的页面
homePageIsTabBar: true,
// 返回键
backIcon: '../../static/img/public/back.png',
// 主页
homeIcon: '../../static/img/public/home.png'
}
export default RefreshOption

View File

@@ -0,0 +1,275 @@
<template>
<view class="page-body" :style="{height:'calc(100vh - '+removeHeight+')'}">
<view class='wrapper'>
<view class="editor-wrapper">
<editor id="editor" class="ql-container" :placeholder="placeholder" showImgSize showImgToolbar
showImgResize @statuschange="onStatusChange" :read-only="readOnly" @ready="onEditorReady"
@input="getEditorContent">
</editor>
</view>
<view class='toolbar' @tap="format" style="height: 120px;overflow-y: auto;">
<view class="iconfont icon-undo" @tap="undo"></view>
<view class="iconfont icon-redo" @tap="redo"></view>
<view :class="formats.bold ? 'ql-active' : ''" class="iconfont icon-zitijiacu" data-name="bold">
</view>
<view :class="formats.italic ? 'ql-active' : ''" class="iconfont icon-zitixieti" data-name="italic">
</view>
<view :class="formats.underline ? 'ql-active' : ''" class="iconfont icon-zitixiahuaxian"
data-name="underline"></view>
<view :class="formats.strike ? 'ql-active' : ''" class="iconfont icon-zitishanchuxian"
data-name="strike"></view>
<view :class="formats.color === '#0000ff' ? 'ql-active' : ''" class="iconfont icon-text_color"
data-name="color" data-value="#0000ff"></view>
<view :class="formats.backgroundColor === '#00ff00' ? 'ql-active' : ''"
class="iconfont icon-fontbgcolor" data-name="backgroundColor" data-value="#00ff00"></view>
<view class="iconfont icon-charutupian" @tap="insertImage"></view>
<view class="iconfont icon-shanchu" @tap="clear"></view>
<view :class="formats.align === 'left' ? 'ql-active' : ''" class="iconfont icon-zuoduiqi"
data-name="align" data-value="left"></view>
<view :class="formats.align === 'center' ? 'ql-active' : ''" class="iconfont icon-juzhongduiqi"
data-name="align" data-value="center"></view>
<view :class="formats.align === 'right' ? 'ql-active' : ''" class="iconfont icon-youduiqi"
data-name="align" data-value="right"></view>
<view :class="formats.align === 'justify' ? 'ql-active' : ''" class="iconfont icon-zuoyouduiqi"
data-name="align" data-value="justify"></view>
<view :class="formats.lineHeight ? 'ql-active' : ''" class="iconfont icon-line-height"
data-name="lineHeight" data-value="2"></view>
<view :class="formats.letterSpacing ? 'ql-active' : ''" class="iconfont icon-Character-Spacing"
data-name="letterSpacing" data-value="2em"></view>
<view :class="formats.marginTop ? 'ql-active' : ''" class="iconfont icon-722bianjiqi_duanqianju"
data-name="marginTop" data-value="20px"></view>
<view :class="formats.previewarginBottom ? 'ql-active' : ''" class="iconfont icon-723bianjiqi_duanhouju"
data-name="marginBottom" data-value="20px"></view>
<view :class="formats.fontSize === '24px' ? 'ql-active' : ''" class="iconfont icon-fontsize"
data-name="fontSize" data-value="24px"></view>
<view :class="formats.list === 'ordered' ? 'ql-active' : ''" class="iconfont icon-youxupailie"
data-name="list" data-value="ordered"></view>
<view :class="formats.list === 'bullet' ? 'ql-active' : ''" class="iconfont icon-wuxupailie"
data-name="list" data-value="bullet"></view>
<view class="iconfont icon-outdent" data-name="indent" data-value="-1"></view>
<view class="iconfont icon-indent" data-name="indent" data-value="+1"></view>
<view class="iconfont icon-fengexian" @tap="insertDivider"></view>
<view :class="formats.script === 'sub' ? 'ql-active' : ''" class="iconfont icon-zitixiabiao"
data-name="script" data-value="sub"></view>
<view :class="formats.script === 'super' ? 'ql-active' : ''" class="iconfont icon-zitishangbiao"
data-name="script" data-value="super"></view>
<view :class="formats.direction === 'rtl' ? 'ql-active' : ''" class="iconfont icon-direction-rtl"
data-name="direction" data-value="rtl"></view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
html: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
removeHeight: {
type: String,
default: '0px'
}
},
data() {
return {
readOnly: false,
formats: {},
result: {
html: "",
text: "",
delta: {}
}
}
},
methods: {
readOnlyChange() {
this.readOnly = !this.readOnly
},
onEditorReady() {
// #ifdef APP-PLUS
uni.createSelectorQuery().select('#editor').context((res) => {
this.editorCtx = res.context
let _this = this
let EContent = {
html: this.html
}
//设置富文本编辑器的内容
this.editorCtx.setContents(EContent);
this.editorCtx.getContents({
complete: res => {
_this.result = res
}
})
}).exec()
// #endif
// #ifdef H5 || MP-WEIXIN
this.createSelectorQuery().select('#editor').context((res) => {
this.editorCtx = res.context
let _this = this
let EContent = {
html: this.html
}
//设置富文本编辑器的内容
this.editorCtx.setContents(EContent);
this.editorCtx.getContents({
complete: res => {
_this.result = res
}
})
}).exec()
// #endif
},
undo() {
this.editorCtx.undo()
},
redo() {
this.editorCtx.redo()
},
format(e) {
let {
name,
value
} = e.target.dataset
if (!name) return
this.editorCtx.format(name, value)
},
onStatusChange(e) {
const formats = e.detail
this.formats = formats
},
insertDivider() {
this.editorCtx.insertDivider({
success: function() {
console.log('insert divider success')
}
})
},
clear() {
this.editorCtx.clear({
success: function(res) {
console.log("clear success")
}
})
},
removeFormat() {
this.editorCtx.removeFormat()
},
insertDate() {
const date = new Date()
const formatDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
this.editorCtx.insertText({
text: formatDate
})
},
insertImage() {
let that = this
console.log('00000>>')
uni.chooseImage({
count: 1,
success: (res) => {
console.log('1111>>'+res.tempFilePaths[0])
that.$emit("insertPic", res.tempFilePaths[0])
}
})
},
insertUrlImage(imageUrl) {
console.log('22222>>'+imageUrl)
this.editorCtx.insertImage({
src: imageUrl,
alt: '图像',
success: function() {
console.log('insert image success')
}
})
},
getEditorContent(e) {
this.result = e.detail;
},
getHtml() {
return this.result.html
},
getText() {
return this.result.text
}
}
}
</script>
<style lang="scss">
@import "./editor-icon.css";
.page-body {
background-color: #4CD964;
.wrapper {
height: 100%;
background-color: #FFFFFF;
box-sizing: border-box;
}
.editor-wrapper {
height: calc(100% - 120px);
background: #fff;
}
.toolbar {
box-sizing: border-box;
border-bottom: 0;
background-color: #F1F1F1;
}
}
.iconfont {
display: inline-block;
padding: 8px 8px;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 20px;
}
.ql-container {
box-sizing: border-box;
padding: 12px 15px;
width: 100%;
min-height: 30vh;
height: 100%;
font-size: 16px;
line-height: 1.5;
}
.ql-active {
color: #06c;
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,30 @@
## 参数
| 参数 | 类型 | 解释 | 默认 | 必填 |
| ------------ | ------------ | ------------ | ------------ | ------------ |
| html | String | 默认html(模板内容) | "" | 否 |
| placeholder | String | 提示信息 | "" | 否 |
| removeHeight | String | 移除高度(默认占满屏幕高度) | "0px" | 否 |
## 方法
| 方法名 | 解释 | 参数 |
| ------------ | ------------ | ------------ |
| getHtml | 获取富文本html | 无 |
| getText | 获取富文本text | 无 |
| insertUrlImage | 插入网络图片 | 图片地址 |
## 事件
| 事件名 | 解释 | 参数 |
| ------------ | ------------ | ------------ |
| insertPic | 用户选取本地图片成功后回调 | 选择的本地图片地址 |
## 支持平台
| app | 微信小程序 |
| ------------ | ------------ |
| 未测试 | 支持 |
| h5-Safari | Android Browser | 微信浏览器(Android) |QQ浏览器(Android) |
| ------------ | ------------ | ------------ | |
| 未测试 | 支持 | 支持 | 支持 |

View File

@@ -0,0 +1,108 @@
<template>
<view :style="{'border-radius': '5rpx','height': '65rpx','width': '160rpx','text-align': 'center','line-height': '65rpx','font-size': '26rpx',
'background-color': background,'color': 'white'}"
@click="click">
{{textDetail}}
</view>
</template>
<script>
export default {
data() {
return {
'background': '#2fa1f0',
"isCanClick": true,
"textDetail": "获取验证码",
"countdown": 60,
'cookie': ''
};
},
methods: {
getCookie() {
return this.cookie;
},
click() {
if (!this.isCanClick) {
return
}
if (this.phoneNum.length != 11 || !this.phoneNum.startsWith("1")) {
this.Toast("请输入合法的手机号")
} else {
// 更改样式并且不可点击
this.background = "gray"
this.isCanClick = false
let timeOut = setInterval(() => {
if (this.countdown == 1) {
this.textDetail = "获取验证码"
this.countdown = 60
clearInterval(timeOut)
this.background = "#2fa1f0"
this.isCanClick = true
} else {
this.countdown = this.countdown - 1;
this.textDetail = this.countdown + "s"
}
}, 1000)
let _this = this
// if (this.sendByCookie != null) {
// // 此方法会判断是否是小程序小程序才执行cookie
// this.HttpCookie({
// 'url': this.url,
// 'data': {
// "mobile": this.phoneNum
// },
// cookie: this.sendByCookie,
// loading: true
// }).then((res) => {
// _this.cookie = res.data
// }, (err) => {
// clearInterval(timeOut)
// this.countdown = 60
// // 失败重置状态
// this.background = "#2fa1f0"
// this.isCanClick = true
// this.textDetail = "获取验证码"
// })
// } else {
_this.HTTP({
url: _this.url+"?mobile="+_this.phoneNum,
data: {},
method: 'GET',
paramsType: "FORM",
loading: true
}).then((res) => {
_this.cookie = res.data
console.log('=======', res)
}, (err) => {
clearInterval(timeOut)
this.countdown = 60
// 失败重置状态
this.background = "#2fa1f0"
this.isCanClick = true
this.textDetail = "获取验证码"
})
// }
}
}
},
props: ['phoneNum', 'url', 'data', 'sendByCookie'],
}
</script>
<style>
</style>

View File

@@ -0,0 +1,245 @@
<template>
<view class="container999" @touchstart="refreshStart" @touchmove="refreshMove" @touchend="refreshEnd">
<!-- 刷新组件需搭配scroll-view使用并在pages.json中添加 "disableScroll":true-->
<tabRefresh ref="tabRefresh" @isRefresh='isRefresh'></tabRefresh>
<view class='nav'>
<!-- 状态栏 -->
<view :style="{'height':statusBarHeightPx}"></view>
<!-- 导航栏 -->
<view
:style="{'height':navHeightPx,'width':navWidthPx,'display': 'flex','flex-direction':'row','box-sizing': 'border-box','justify-content': 'space-between'}">
<view
:style="{'height':navHeightPx,'display': 'flex','flex-direction':'row','box-sizing': 'border-box'}">
<image :style="{'height':navHeightPx,'width':iconPx}" mode="aspectFit" :src="backIcon"
@click="clickBack"></image>
<text
:style="{'height':navHeightPx,'line-height':navHeightPx,'font-size':'33rpx','color':'#FFFFFF'}">{{text}}</text>
</view>
<view
:style="{'height':navHeightPx,'display': 'flex','flex-direction':'row','box-sizing': 'border-box'}">
<view v-if="useTitleLeftBtn!=0"
:style="{'height':navHeightPx,'display': 'flex','flex-direction':'row','box-sizing': 'border-box'}"
@click="clickLeftBtn">
<image v-if="useTitleLeftBtn == 2" :style="{'height':navHeightPx,'width':navHeightPx}"
mode="aspectFit" :src="titleLeftBtnSource"></image>
<text v-if="useTitleLeftBtn == 1"
:style="{'height':navHeightPx,'line-height':navHeightPx,'font-size':'28rpx','color':'#FFFFFF','padding-left':'20rpx','padding-right':'20rpx'}">{{titleLeftBtnSource}}</text>
</view>
<view v-if="useTitleRightBtn!=0"
:style="{'height':navHeightPx,'display': 'flex','flex-direction':'row','box-sizing': 'border-box'}"
@click="clickRightBtn">
<image v-if="useTitleRightBtn==2" :style="{'height':navHeightPx,'width':navHeightPx}"
mode="aspectFit" :src="titleRightBtnSource"></image>
<text v-if="useTitleRightBtn==1"
:style="{'height':navHeightPx,'line-height':navHeightPx,'font-size':'28rpx','color':'#FFFFFF','padding-left':'20rpx','padding-right':'20rpx'}">{{titleRightBtnSource}}</text>
</view>
</view>
</view>
<!-- Tab切换 -->
<tabTop ref="navTab" :tabTitle="tabTitleData" :tabNum="tabNumData" @onTabClick='clickTab'></tabTop>
</view>
<slot></slot>
</view>
</template>
<script>
const util = require('@/util/util.js');
export default {
props: {
text: {
type: String,
default: ""
},
isShareIn: {
type: Boolean,
default: false
},
hasBack: {
type: Boolean,
default: true
},
useTitleLeftBtn: {
type: String,
// 0不使用 1使用文字 2使用图片
default: "0"
},
useTitleRightBtn: {
type: String,
// 0不使用 1使用文字 2使用图片
default: "0"
},
titleLeftBtnSource: {
type: String,
default: ""
},
titleRightBtnSource: {
type: String,
default: ""
},
tabTitleData: {
type: Array,
default () {
return []
}
},
tabNumData: {
type: Array,
default () {
return []
}
}
},
data() {
return {
currentTab: 0, //sweiper所在页
swiperPaddindTop: 0,
windowHeight: 0, // 可使用窗口的高度
statusBarH: 0, // 状态栏高度
statusBarHeight: "",
navHeight: "",
navWidth: "",
totalHeight: "",
statusBarHeightPx: "",
navHeightPx: "",
navWidthPx: "",
totalHeightPx: "",
bg: "#2fa1f0",
totalHeightUpx: "",
backIcon: "",
iconPx: ""
};
},
methods: {
getViewPagerTop() {
return this.swiperPaddindTop
},
clickLeftBtn() {
this.$emit("leftBtn", "左侧按钮")
},
clickRightBtn() {
this.$emit("rightBtn", "右侧按钮")
},
clickBack() {
if (this.hasBack) {
if (this.$props.isShareIn) {
uni.switchTab({
url: "../../pages/home/FindFragment"
})
} else {
this.Back()
}
}
},
// 刷新touch监听
refreshStart(e) {
this.$refs.tabRefresh.refreshStart(e);
},
refreshMove(e) {
this.$refs.tabRefresh.refreshMove(e);
},
refreshEnd(e) {
this.$refs.tabRefresh.refreshEnd(e);
},
isRefresh() {
this.$emit("downRefresh", this.currentTab)
},
// 下拉刷新完成主动调用
downRefresh() {
this.$refs.tabRefresh.endAfter()
},
// 主动点击tab
clickTab(index) {
// 对外提供点击的index(使用控件的页面更改data里currentTab值)
// 保证能跟着滚动
this.currentTab = index
this.$emit("tabClickItem", index)
}, // swiper 滑动
changeTab(e) {
var index = e.detail.current //获取索引
// 模拟tab点击
this.$refs.navTab.longClick(index);
}
},
watch: {
isShareIn(newValue) {
if (this.$props.hasBack) {
this.backIcon = '../../static/custom-icon/back.png'
this.iconPx = this.navHeightPx
if (this.$props.isShareIn) {
this.backIcon = '../../static/custom-icon/home.png'
}
} else {
this.backIcon = '../../static/img/public/no-back.png'
this.iconPx = this.navHeight / 8 * 3 + 'px'
}
}
},
created() {
this.navWidth = getApp().globalData.navWidth
this.navHeight = getApp().globalData.navHeight
this.totalHeight = getApp().globalData.totalHeight
this.statusBarHeight = getApp().globalData.statusBarHeight
this.navWidthPx = getApp().globalData.navWidthPx
this.navHeightPx = getApp().globalData.navHeightPx
this.totalHeightPx = getApp().globalData.totalHeightPx
this.statusBarHeightPx = getApp().globalData.statusBarHeightPx
this.totalHeightUpx = getApp().globalData.totalHeightUpx
if (this.$props.hasBack) {
this.backIcon = '../../static/img/public/back.png'
this.iconPx = this.navHeightPx
if (this.$props.isShareIn) {
this.backIcon = '../../static/img/public/home.png'
}
} else {
this.backIcon = '../../static/custom-icon/no-back.png'
this.iconPx = this.navHeight / 8 * 3 + 'px'
}
this.swiperPaddindTop = this.totalHeight + 45 + 'px'
}
}
</script>
<style>
.container999 {
width: 100vw;
font-size: 28upx;
min-height: 100vh;
overflow: hidden;
color: #6B8082;
position: relative;
}
.nav {
position: fixed;
left: 0;
top: 0;
color: white;
width: 100%;
display: flex;
flex-direction: column;
font-size: 24upx;
background-color: #2fa1f0;
z-index: 996;
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<view class="user-item-content" @click="click">
<view class="user-item-left">
<image :src="src" style="width: 70rpx;height: 70rpx;" mode="aspectFit"></image>
<text class="user-item-textBlack">{{text}}</text>
</view>
<view class="user-item-right" style="display: flex;align-items: center;">
<text v-if="showRightText"
style="font-size:30rpx ;color: #999999;margin-top: -7rpx;">{{rightText}}</text>
<image v-if="showRightImg" style="height: 30rpx;width: 30rpx;" src="../../static/img/public/more.png"></image>
</view>
</view>
</template>
<script>
export default {
props: {
src: {
type: String,
default: ""
},
text: {
type: String,
default: ""
},
clickId: {
type: String,
default: "0"
},
rightText: {
type: String,
default: ""
},
showRightText: {
type: Boolean,
default: false
},
showRightImg: {
type: Boolean,
default: true
},
},
data() {
return {
};
},
methods: {
click() {
var clickId = this.$props.clickId;
this.$emit("click", clickId)
}
}
}
</script>
<style lang="scss">
.user-item-content {
width: 100%;
height: 90rpx;
display: flex;
flex-direction: row;
align-items: center;
padding-left: 40rpx;
justify-content: space-between;
border-bottom: 0.1px solid #F1F1F1;
box-sizing: border-box;
.user-item-left {
display: flex;
flex-direction: row;
height: 89rpx;
align-items: center;
flex: 1;
box-sizing: border-box;
.user-item-textBlack {
color: #333333;
font-size: 30rpx;
height: 89rpx;
line-height: 89rpx;
}
}
.user-item-right {
margin-right: 35rpx;
flex-shrink: 0;
}
}
</style>

View File

@@ -0,0 +1,516 @@
<!--
* @插件日期时间选择器
* @作者陈万照
* @公司山东标梵互动信息技术有限公司
* @官网http://biaofun.com/
* @微信C207668802
* @QQ207668802
* @邮箱cwz@biaofun.com || 207668802@qq.com
* @版本v1.0.8
-->
<template>
<view>
<picker :mode="mode" :range="range" range-key="text" @change="change" @columnchange="columnchange"
:value="value" :disa:bled="disabled">
<slot></slot>
</picker>
</view>
</template>
<script>
import utils from '@/common/utils.js'; // 封装的工具集
export default {
/**
* 数据
*/
props: {
// 类型
mode: {
type: String,
default: "multiSelector"
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 占位符
placeholder: {
type: String,
default: '请选择日期时间'
},
// 表示有效日期时间范围的开始,
// 字符串格式为 "YYYY-MM-DD hh:mm"
start: {
type: String,
default: '1970-1-1 00:00'
},
// 表示有效日期时间范围的结束
// 字符串格式为 "YYYY-MM-DD hh:mm"
end: {
type: String,
default: '2300-1-1 00:00'
},
// 表示选择器的粒度有效值year | month | day | hour | minute
fields: {
type: String,
default: 'minute'
},
// 默认值
// 字符串格式为 "YYYY-MM-DD hh:mm"
defaultValue: {
type: String,
default: ''
}
},
/**
* 数据
*/
data() {
return {
range: [],
value: [],
dateStr: '', // 最终显示的字符串
dtStart: null, // 有效范围开始
dtEnd: null, // 有效范围结束
};
},
/**
* 监听数据
*/
watch: {
// 默认值
defaultValue() {
// 设置默认值
this.setDefaultValue();
}
},
/**
* 组件初次加载完成
*/
mounted() {
// 有效日期开始和结束
let start = this.start;
let end = this.end;
// 验证是否是有效的开始和结束日期
if (!utils.isString(this.start)) {
console.log('开始日期需为String类型格式为 "YYYY-MM-DD hh:mm"');
start = '1970-1-1 00:00';
}
if (!utils.isString(this.start)) {
console.log('结束日期需为String类型格式为 "YYYY-MM-DD hh:mm"');
start = '2300-1-1 00:00';
}
// 将开始日期和结束日期转为 Date
let dtStart = utils.formatDate(start).dt;
let dtEnd = utils.formatDate(end).dt;
// 判断有效日期结束是否大于有效日期开始如果不是则将有效日期结束修改为有效日期开始往后300年
if (dtEnd <= dtStart) {
dtEnd = utils.formatDate(start).dt;
dtEnd.setFullYear(dtStart.getFullYear() + 300);
dtEnd.setDate(dtEnd.getDate() - 1);
}
// 更新开始日期和结束日期
this.dtStart = dtStart;
this.dtEnd = dtEnd;
// 设置默认值
this.setDefaultValue();
},
/**
* 方法
*/
methods: {
/**
* 确认选择
*/
change(event) {
let year, month, day, hour, minute;
if (this.fields == 'year') {
year = this.range[0][this.value[0]].number; // 年
let dtStr = `${year}`;
this.setDateStr(dtStr);
this.$emit('change', utils.formatDate(dtStr));
return;
} else if (this.fields == 'month') {
year = this.range[0][this.value[0]].number; // 年
month = this.range[1][this.value[1]].number; // 月
let dtStr = `${year}-${month}`;
this.setDateStr(dtStr);
this.$emit('change', utils.formatDate(dtStr));
return;
} else if (this.fields == 'day') {
year = this.range[0][this.value[0]].number; // 年
month = this.range[1][this.value[1]].number; // 月
day = this.range[2][this.value[2]].number; // 日
let dtStr = `${year}-${month}-${day}`;
this.setDateStr(dtStr);
this.$emit('change', utils.formatDate(dtStr));
return;
} else if (this.fields == 'hour') {
year = this.range[0][this.value[0]].number; // 年
month = this.range[1][this.value[1]].number; // 月
day = this.range[2][this.value[2]].number; // 日
hour = this.range[3][this.value[3]].number; // 时
day = this.range[2][this.value[2]].number; // 日
let dtStr = `${year}-${month}-${day} ${hour}`;
this.setDateStr(dtStr);
this.$emit('change', utils.formatDate(dtStr));
return;
} else if (this.fields == 'minute') {
year = this.range[0][this.value[0]].number; // 年
month = this.range[1][this.value[1]].number; // 月
day = this.range[2][this.value[2]].number; // 日
hour = this.range[3][this.value[3]].number; // 时
minute = this.range[4][this.value[4]].number; // 分
let dtStr = `${year}-${month}-${day} ${hour}:${minute}`;
this.setDateStr(dtStr);
this.$emit('change', utils.formatDate(dtStr));
return;
}
},
/**
* 设置显示的值
* @param {Date|String} date 日期字符串或日期对象
*/
setDateStr(date) {
let dt = utils.formatDate(date);
if (this.fields == 'year') {
this.dateStr = `${dt.YYYY}`;
return;
}
if (this.fields == 'month') {
this.dateStr = `${dt.YYYY}${dt.M}`;
return;
}
if (this.fields == 'day') {
this.dateStr = `${dt.YYYY}${dt.M}${dt.D}`;
return;
}
if (this.fields == 'hour') {
this.dateStr = `${dt.YYYY}${dt.M}${dt.D}${dt.h}`;
return;
}
this.dateStr = `${dt.YYYY}${dt.M}${dt.D}${dt.h}${dt.m}`;
},
/**
* 设置年数据
*/
setYearData() {
// 有效日期
let yearStart = this.dtStart.getFullYear();
let yearEnd = this.dtEnd.getFullYear();
// 年
let years = [];
for (let year = yearStart; year <= yearEnd; year++) {
let item = {
number: year,
text: `${year}`,
};
years.push(item);
}
this.range.splice(0, 1, years);
},
/**
* 设置月数据
* @param {Number} year 年
*/
setMonthData(year) {
// 有效日期
let yearStart = this.dtStart.getFullYear();
let monthStart = this.dtStart.getMonth() + 1;
let yearEnd = this.dtEnd.getFullYear();
let monthEnd = this.dtEnd.getMonth() + 1;
// 月
let months = [];
let monthStartIndex = year == yearStart ? monthStart : 1;
let monthEndIndex = year == yearEnd ? monthEnd : 12;
for (let month = monthStartIndex; month <= monthEndIndex; month++) {
let item = {
number: month,
text: `${month}`,
};
months.push(item);
}
this.range.splice(1, 1, months);
},
/**
* 设置日数据
* @param {Number} year 年
* @param {Number} month 月
*/
setDayData(year, month) {
// 有效日期
let yearStart = this.dtStart.getFullYear();
let monthStart = this.dtStart.getMonth() + 1;
let dayStart = this.dtStart.getDate();
let yearEnd = this.dtEnd.getFullYear();
let monthEnd = this.dtEnd.getMonth() + 1;
let dayEnd = this.dtEnd.getDate();
// 日
let days = [];
let dayStartIndex = year == yearStart && month == monthStart ? dayStart : 1;
let dayEndIndex;
if (year == yearEnd && month == monthEnd) {
dayEndIndex = dayEnd;
} else {
dayEndIndex = (new Date(year, month, 0)).getDate();
}
for (let day = dayStartIndex; day <= dayEndIndex; day++) {
let item = {
number: day,
text: `${day}`,
};
days.push(item);
}
this.range.splice(2, 1, days);
},
/**
* 设置时数据
* @param {Number} year 年
* @param {Number} month 月
* @param {Number} day 日
*/
setHourData(year, month, day) {
// 有效日期
let yearStart = this.dtStart.getFullYear();
let monthStart = this.dtStart.getMonth() + 1;
let dayStart = this.dtStart.getDate();
let hourStart = this.dtStart.getHours();
let yearEnd = this.dtEnd.getFullYear();
let monthEnd = this.dtEnd.getMonth() + 1;
let dayEnd = this.dtEnd.getDate();
let hourEnd = this.dtEnd.getHours();
// 时
let hours = [];
let hourStartIndex = year == yearStart && month == monthStart && day == dayStart ? hourStart : 0;
let hourEndIndex = year == yearEnd && month == monthEnd && day == dayEnd ? hourEnd : 23;
for (let hour = hourStartIndex; hour <= hourEndIndex; hour++) {
let item = {
number: hour,
text: `${hour}`,
};
hours.push(item);
}
this.range.splice(3, 1, hours);
},
/**
* 设置分数据
* @param {Number} year 年
* @param {Number} month 月
* @param {Number} day 日
* @param {Number} hour 时
*/
setMinuteData(year, month, day, hour) {
// 有效日期
let yearStart = this.dtStart.getFullYear();
let monthStart = this.dtStart.getMonth() + 1;
let dayStart = this.dtStart.getDate();
let hourStart = this.dtStart.getHours();
let minuteStart = this.dtStart.getMinutes();
let yearEnd = this.dtEnd.getFullYear();
let monthEnd = this.dtEnd.getMonth() + 1;
let dayEnd = this.dtEnd.getDate();
let hourEnd = this.dtEnd.getHours();
let minuteEnd = this.dtEnd.getMinutes();
// 分
let minutes = [];
let minuteStartIndex = year == yearStart && month == monthStart && day == dayStart && hour == hourStart ?
minuteStart : 0;
let minuteEndIndex = year == yearEnd && month == monthEnd && day == dayEnd && hour == hourEnd ? minuteEnd :
59;
for (let minute = minuteStartIndex; minute <= minuteEndIndex; minute++) {
let item = {
number: minute,
text: `${minute}`,
}
minutes.push(item);
}
this.range.splice(4, 1, minutes);
},
/**
* 设置默认值
*/
setDefaultValue() {
// 默认日期
let dtDefault;
// 开始日期和结束日期
let dtStart = this.dtStart;
let dtEnd = this.dtEnd;
// 判断是否传了默认日期
// 传了默认日期,格式化默认日期为日期对象
if (this.defaultValue) {
dtDefault = utils.formatDate(this.defaultValue).dt;
}
// 如果没有传默认日期,将默认日期设置为当前日期
else {
dtDefault = new Date();
}
// 如果默认日期不在有效日期范围内,设置默认日期为有效日期开始值
if (dtDefault < dtStart || dtDefault > dtEnd) {
dtDefault = dtStart;
}
// 更新 dateStr
if (this.defaultValue) this.setDateStr(dtDefault);
// 默认值相关数据
let dfYear = dtDefault.getFullYear();
let dfMonth = dtDefault.getMonth() + 1;
let dfDay = dtDefault.getDate();
let dfHour = dtDefault.getHours();
let dfMinute = dtDefault.getMinutes();
// 设置年数据
this.setYearData();
// 设置 Year 这一列的 value 值
let yearIndex = this.range[0].findIndex(year => {
return dfYear == year.number;
});
this.value.splice(0, 1, yearIndex >= 0 ? yearIndex : 0);
// 设置月数据
if (this.fields == 'year') return;
this.setMonthData(dfYear);
// 设置 Month 这一列的 value 值
let monthIndex = this.range[1].findIndex(month => {
return dfMonth == month.number;
});
this.value.splice(1, 1, monthIndex >= 0 ? monthIndex : 0);
// 设置日数据
if (this.fields == 'month') return;
this.setDayData(dfYear, dfMonth);
// 设置 Day 这一列的 value 值
let dayIndex = this.range[2].findIndex(day => {
return dfDay == day.number;
});
this.value.splice(2, 1, dayIndex >= 0 ? dayIndex : 0);
// 设置时数据
if (this.fields == 'day') return;
this.setHourData(dfYear, dfMonth, dfDay);
// 设置 Hour 这一列的 value 值
let hourIndex = this.range[3].findIndex(hour => {
return dfHour == hour.number;
});
this.value.splice(3, 1, hourIndex >= 0 ? hourIndex : 0);
// 设置分数据
if (this.fields == 'hour') return;
this.setMinuteData(dfYear, dfMonth, dfDay, dfHour);
// 设置 Minute 这一列的 value 值
let minuteIndex = this.range[4].findIndex(minute => {
return dfMinute == minute.number;
});
this.value.splice(4, 1, minuteIndex >= 0 ? minuteIndex : 0);
},
/**
* 某一列的值改变时触发
* @param {Number} event.detail.column 表示改变了第几列下标从0开始
* @param {Number} event.detail.value 表示变更值的下标
*/
columnchange(event) {
let columnIndex = event.detail.column; // 改变的列的下标
let valueIndex = event.detail.value; // 变更值的下标
// 更新改变列的 value
this.value.splice(columnIndex, 1, valueIndex);
// 改变年要更新月数据
if (this.fields == 'year') return;
if (columnIndex == 0) {
// 当前选择的月
let monthBeforeUpdate = this.range[1][this.value[1]];
// 更新月数据
this.setMonthData(this.range[0][this.value[0]].number);
// 更新 Month Value
let monthIndex = this.range[1].findIndex(month => {
return month.number == monthBeforeUpdate.number;
});
this.value.splice(1, 1, monthIndex >= 0 ? monthIndex : 0);
}
// 改变年、月都要更新日数据
if (this.fields == 'month') return;
if (columnIndex == 0 || columnIndex == 1) {
// 当前选择的日
let dayBeforeUpdate = this.range[2][this.value[2]];
// 更新日数据
this.setDayData(this.range[0][this.value[0]].number, this.range[1][this.value[1]].number);
// 更新 Day Value
let dayIndex = this.range[2].findIndex(day => {
return day.number == dayBeforeUpdate.number;
});
this.value.splice(2, 1, dayIndex >= 0 ? dayIndex : 0);
}
// 改变年、月、日都要更新时数据
if (this.fields == 'day') return;
if (columnIndex == 0 || columnIndex == 1 || columnIndex == 2) {
// 当前选择的时
let hourBeforeUpdate = this.range[3][this.value[3]];
// 更新时数据
this.setHourData(this.range[0][this.value[0]].number, this.range[1][this.value[1]].number, this.range[
2][this.value[2]].number);
// 更新 Hour Value
let hourIndex = this.range[3].findIndex(hour => {
return hour.number == hourBeforeUpdate.number;
});
this.value.splice(3, 1, hourIndex >= 0 ? hourIndex : 0);
}
// 当前选择的分
if (this.fields == 'hour') return;
let minuteBeforeUpdate = this.range[4][this.value[4]];
// 更新分数据
this.setMinuteData(this.range[0][this.value[0]].number, this.range[1][this.value[1]].number, this.range[2][
this.value[2]
].number, this.range[3][this.value[3]].number);
// 更新 Minute Value
let minuteIndex = this.range[4].findIndex(minute => {
return minute.number == minuteBeforeUpdate.number;
});
this.value.splice(4, 1, minuteIndex >= 0 ? minuteIndex : 0);
},
}
};
</script>
<style lang="scss" scoped>
.content {
text-align: right;
}
.placeholder {
color: #949596;
}
</style>

View File

@@ -0,0 +1,30 @@
### 组件说明
* 日期时间选择器
* 组件的默认日期有效期范围为:"1970-01-01 00:00" - "2300-01-01 00:00"。
* 注意:如果您传递的日期有效范围的结束日期小于开始日期,则日期有效范围的结束日期会自动修正为开始日期+300年比如您传递的日期有效范围的开始日期为 "2020-11-11 18:30"
* 结束日期为 "2018-11-11 18:30",则此时将会自动修正结束日期为 "2320-11-11 18:30"。
* 注意:如果您传递的默认值不在日期有效范围内,则会自动修正默认值为当前日期时间,如果当前日期时间也不在日期有效范围内,则会再次修正为日期有效范围的开始日期。
* 注意:该组件用到了我自己封装的 utils.js位置在 '@/common/js/utils.js'。
### 插件 props 属性
* disabled: 是否禁用该组件Boolean类型
* placeholder: 组件没有选中值时显示的内容String类型
* start: 表示有效日期时间范围的开始String类型格式为 "YYYY-MM-DD hh:mm"
* end: 表示有效日期时间范围的结束String类型格式必为 "YYYY-MM-DD hh:mm"
* fields: 选择器的粒度String类型有效值为 year、month、day、hour、minute
* defaultValue: 默认值String类型格式为 "YYYY-MM-DD hh:mm"
### 插件事件
- change(date):选择日期时间后的回调事件。
* date.YYYY: 年;
* date.M: 月;
* date.MM: 月补0
* date.D: 日;
* date.DD: 日补0
* date.h: 时;
* date.hh: 时补0
* date.m: 分;
* date.mm: 分补0
* date.dt: Date对象
* ... 还有一些其他的字段,具体看返回值吧!

View File

@@ -0,0 +1,422 @@
<template>
<!-- 城市选择-->
<view class="city-select">
<StatusBar></StatusBar>
<!-- 预留搜索-->
<view style="background-color: #FFFFFF;">
<view :style="{'height':navHeight,'display': 'flex','box-sizing': 'border-box','flex-direction':'row',
'align-items':'center','width':width}">
<view class="city-serach" v-if="isSearch">
<input @input="keyInput" placeholder="请输入城市名称">
</view>
<text style="font-size: 27rpx;margin-right: 20rpx;color: #999999;border-left: 1px solid #f1f1f1;padding-left: 20rpx;"
@click="close">取消</text>
</view>
</view>
<scroll-view :scroll-top="scrollTop" scroll-y="true" class="city-select-main" id="city-select-main">
<!-- 当前定位城市 -->
<view class="hot-title" v-if="activeCity && !serachCity">当前定位城市</view>
<view class="hot-city" v-if="activeCity && !serachCity">
<view class="hot-item" @click="cityTrigger(activeCity)">{{ activeCity[formatName] }}</view>
</view>
<!-- 热门城市 -->
<view class="hot-title" v-if="hotCity.length > 0 && !serachCity">热门城市</view>
<view class="hot-city" v-if="hotCity.length > 0 && !serachCity">
<template v-for="(item, index) in hotCity">
<view :key="index" @click="cityTrigger(item, 'hot')" class="hot-item">{{ item[formatName] }}</view>
</template>
</view>
<!-- 城市列表(搜索前) -->
<view class="citys" v-if="!serachCity">
<view v-for="(city, index) in sortItems" :key="index" v-if="city.isCity">
<view class="citys-item-letter" :id="'city-letter-' + (city.name === '#' ? '0' : city.name)">{{ city.name }}</view>
<view class="citys-item" v-for="(item, inx) in city.citys" :key="inx" @click="cityTrigger(item)">{{ item.cityName }}</view>
</view>
</view>
<!-- 城市列表(搜索后) -->
<view class="citys" v-if="serachCity">
<view v-for="(item, index) in searchDatas" :key="index">
<view class="citys-item" :key="index" @click="cityTrigger(item)">{{ item.name }}</view>
</view>
</view>
</scroll-view>
<!-- 城市选择索引-->
<view class="city-indexs-view" v-if="!serachCity">
<view class="city-indexs">
<view v-for="(cityIns, index) in handleCity" v-if="cityIns.isCity" :key="index" @click="cityindex(cityIns.name)">{{ cityIns.name }}</view>
</view>
</view>
</view>
</template>
<script>
import citySelect from './citySelect.js'
export default {
props: {
//传入要排序的名称
formatName: {
type: String,
default: 'cityName'
},
//当前定位城市
activeCity: {
type: Object,
default: () => null
},
//热门城市
hotCity: {
type: Array,
default: () => []
},
//城市数据
obtainCitys: {
type: Array,
default: () => []
},
//是否有搜索
isSearch: {
type: Boolean,
default: true
}
},
data() {
return {
navHeight: "",
width: "",
totalHeight: "",
scrollTop: 0, //scroll-view 滑动的距离
cityindexs: [], // 城市索引
activeCityIndex: '', // 当前所在的城市索引
handleCity: [], // 处理后的城市数据
serachCity: '', // 搜索的城市
cityData: []
}
},
computed: {
/**
* @desc 城市列表排序
* @return Array
*/
sortItems() {
for (let index = 0; index < this.handleCity.length; index++) {
if (this.handleCity[index].isCity) {
var cityArr = this.handleCity[index].citys
cityArr = cityArr.sort(function(a, b) {
var value1 = a.unicode
var value2 = b.unicode
return value1 - value2
})
}
}
return this.handleCity
},
/**
* @desc 搜索后的城市列表
* @return Array
*/
searchDatas() {
var searchData = []
for (let i = 0; i < this.cityData.length; i++) {
if (this.cityData[i][this.formatName].indexOf(this.serachCity) !== -1) {
searchData.push({
oldData: this.cityData[i],
name: this.cityData[i][this.formatName]
})
}
}
return searchData
}
},
created() {
// 初始化城市数据
this.cityData = this.obtainCitys
this.initializationCity()
this.buildCityindexs()
// 获取状态栏高度
let info = uni.getSystemInfoSync()
let statusBarHeight = info.statusBarHeight
this.statusBarHeight = statusBarHeight + 'px'
this.navHeight = '45px'
this.width = '100%'
this.totalHeight = 45 + statusBarHeight + 'px'
// 获取胶囊高度
// #ifdef MP-WEIXIN||MP-BAID||MP-QQ||MP-TOUTIAO
let menuButton = uni.getMenuButtonBoundingClientRect()
let top = menuButton.top
let bottom = menuButton.bottom
let navHeight = bottom - top + (top - statusBarHeight) * 2 + 4
this.navHeight = navHeight + 'px'
this.width = menuButton.left + 'px'
this.totalHeight = navHeight + statusBarHeight + 'px'
// #endif
},
watch: {
obtainCitys(newData) {
this.updateCitys(newData)
}
},
methods: {
/**
* @desc 初始化
*/
updateCitys(data) {
if (data && data.length) {
this.cityData = data
this.initializationCity()
this.buildCityindexs()
}
},
/**
* @desc 监听输入框的值
*/
keyInput(event) {
this.serachCity = event.detail.value
},
/**
* @desc 初始化城市数据
* @return undefind
*/
initializationCity() {
this.handleCity = []
const cityLetterArr = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '#'
]
for (let index = 0; index < cityLetterArr.length; index++) {
this.handleCity.push({
name: cityLetterArr[index],
isCity: false, // 用于区分是否含有当前字母开头的城市
citys: [] // 存放城市首字母含是此字母的数组
})
}
},
/**
* @desc 得到城市的首字母
* @param str String
*/
getLetter(str) {
return citySelect.getFirstLetter(str[0])
},
/**
* @desc 构建城市索引
* @return undefind
*/
buildCityindexs() {
this.cityindexs = []
for (let i = 0; i < this.cityData.length; i++) {
// 获取首字母
let cityLetter = this.getLetter(this.cityData[i][this.formatName]).firstletter
// 获取当前城市首字母的unicode用作后续排序
let unicode = this.getLetter(this.cityData[i][this.formatName]).unicode
let index = this.cityIndexPosition(cityLetter)
if (this.cityindexs.indexOf(cityLetter) === -1) {
this.handleCity[index].isCity = true
this.cityindexs.push(cityLetter)
}
this.handleCity[index].citys.push({
cityName: this.cityData[i][this.formatName],
unicode: unicode,
oldData: this.cityData[i]
})
}
},
/**
* @desc 滑动到城市索引所在的地方
* @param id String 城市索引
*/
cityindex(id) {
//创建节点查询器
const query = uni.createSelectorQuery().in(this)
var that = this
that.scrollTop = 0
//滑动到指定位置(解决方法:重置到顶部,重新计算,影响:页面会闪一下)
setTimeout(() => {
query
.select('#city-letter-' + (id === '#' ? '0' : id))
.boundingClientRect(data => {
// console.log("得到布局位置信息" + JSON.stringify(data));
// console.log("节点离页面顶部的距离为" + data.top);
data ? (that.scrollTop = data.top) : void 0
})
.exec()
}, 0)
},
/**
* @desc 获取城市首字母的unicode
* @param letter String 城市索引
*/
cityIndexPosition(letter) {
if (!letter) {
return ''
}
const ACode = 65
return letter === '#' ? 26 : letter.charCodeAt(0) - ACode
},
/** @desc 城市列表点击事件
* @param Object
*/
cityTrigger(item, isHot) {
// 传值到父组件
this.$emit('cityClick', item.oldData ? item.oldData : item)
},
close() {
this.Back()
}
}
}
</script>
<style lang="scss">
//宽度转换vw
@function vww($number) {
@return ($number / 375) * 750+rpx;
}
view {
box-sizing: border-box;
}
.city-serach {
flex: 1;
color: #4a4a4a;
padding: 0 vww(10);
&-input {
margin: vww(13) 0;
height: vww(40);
line-height: vww(40);
font-size: vww(14);
padding: 0 vww(5);
border: 1px solid #4d8cfd;
border-radius: 3px;
}
}
.city-select-main {
position: relative;
// overflow: scroll;
// -webkit-overflow-scrolling: touch;
width: 100%;
height: 100%;
background: #FFFFFF;
// overflow-y: auto;
}
.city-select {
position: relative;
width: 100vw;
height: 100vh;
background: #f6f5fa;
// 热门城市
.hot-title {
padding-left: vww(13);
width: 100vw;
font-size: 14px;
line-height: vww(30);
color: #9b9b9b;
background-color: #ededed;
}
.hot-city {
padding-left: vww(23);
padding-right: vww(20);
padding-top: vww(12);
overflow: hidden;
width: 100vw;
.hot-item {
float: left;
padding: 0 vww(5);
margin-right: vww(16);
margin-bottom: vww(12);
overflow: hidden;
width: vww(100);
height: vww(31);
font-size: 14px;
text-align: center;
border-radius: vww(5);
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
line-height: vww(31);
color: #4a4a4a;
background: #f5f5f5;
&:nth-child(3n) {
margin-right: 0;
}
}
.hot-hidden {
display: none;
margin-right: 0;
}
}
.citys {
>view {
padding-left: vww(18);
width: 100%;
font-size: 14px;
background: #fff;
.citys-item-letter {
margin-left: vww(-18);
padding-left: vww(18);
margin-top: -1px;
width: 100vw;
line-height: vww(30);
color: #9b9b9b;
background: #ededed;
border-top: none;
}
.citys-item {
width: 100%;
line-height: vww(50);
color: #4a4a4a;
border-bottom: 0.1px solid #ebebf0;
&:last-child {
border: none;
}
}
}
}
.city-indexs-view {
position: absolute;
right: 0;
top: 0;
z-index: 999;
display: flex;
width: vww(20);
height: 100%;
text-align: center;
.city-indexs {
width: vww(20);
text-align: center;
vertical-align: middle;
align-self: center;
>view {
margin-bottom: vww(10);
width: vww(20);
font-size: 12px;
color: #4d8cfd;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
</style>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,697 @@
<template>
<view class="cropper" id="cropper" :class="{ show: show }">
<view class="cropper-head">
<view class="cropper-btn cropper-reset" @tap="resetCrop">重做</view>
</view>
<view class="cropper-body">
<image id="image" class="cropper-image" :src="imagePath" mode="aspectFit"></image>
<view
:style="{ width: stageWidth + 'px', height: stageHeight + 'px', left: stageLeft + 'px', top: stageTop + 'px' }"
class="cropper-stage" @touchstart.stop.prevent="touchStart" @touchmove.stop.prevent="touchMove">
<view id="box" class="cropper-box"
:style="{ width: boxWidth + 'px', height: boxHeight + 'px', left: boxLeft + 'px', top: boxTop + 'px' }">
<view id="lt" class="lt"></view>
<view id="lb" class="lb"></view>
<view id="rt" class="rt"></view>
<view id="rb" class="rb"></view>
<view class="line-v" style="left:33.3%;"></view>
<view class="line-v" style="left:66.6%;"></view>
<view class="line-h" style="top:33.3%;"></view>
<view class="line-h" style="top:66.6%;"></view>
</view>
</view>
<canvas class="cropper-canvas" canvas-id="canvas"
:style="{ height: canvasHeight + 'px', width: canvasWidth + 'px' }"></canvas>
</view>
<view class="cropper-bottom">
<view class="cropper-btn cropper-cancel" @tap="cancelCrop">取消</view>
<view class="cropper-btn cropper-ok" @tap="completeCrop">裁剪</view>
</view>
</view>
</template>
<script>
//无须渲染的变量
let layoutLeft = 0;
let layoutTop = 0;
let layoutWidth = 0;
let layoutHeight = 0;
let stageLeft = 0;
let stageTop = 0;
let stageWidth = 0;
let stageHeight = 0;
let imageWidth = 0;
let imageHeight = 0;
let pixelRatio = 1; //todo设备像素密度//暂不使用//
let imageStageRatio = 1; //图片实际尺寸与剪裁舞台大小的比值,用于尺寸换算。
let minBoxWidth = 0;
let minBoxHeight = 0;
let touchStartBoxLeft = 0;
let touchStartBoxTop = 0;
let touchStartBoxWidth = 0;
let touchStartBoxHeight = 0;
let touchStartX = 0;
let touchStartY = 0;
export default {
name: 'cropper',
props: {
quality: {
type: Number,
default: 1
},
//目标文件的类型。默认值为jpgjpg输出jpg格式图片png输出png格式图片
outputFileType: {
type: String,
default: 'jpg'
},
//目标图片的宽高比默认null即不限制剪裁宽高比。aspectRatio需大于0
aspectRatio: {
type: [Number, null],
default: null
},
//最小剪裁尺寸与原图尺寸的比率默认0.15即宽度最小剪裁到原图的0.15宽。
minBoxWidthRatio: {
type: Number,
default: 0.15
},
//同minBoxWidthRatio当设置aspectRatio时minBoxHeight值设置无效。minBoxHeight值由minBoxWidth 和 aspectRatio自动计算得到。
minBoxHeightRatio: {
type: Number,
default: 0.15
},
//剪裁框初始大小比率。默认值0.8即剪裁框默认宽度为图片宽度的0.8倍。
initialBoxWidthRatio: {
type: Number,
default: 0.8
},
//同initialBoxWidthRatio当设置aspectRatio时initialBoxHeightRatio值设置无效。initialBoxHeightRatio值由initialBoxWidthRatio 和 aspectRatio自动计算得到。
initialBoxHeightRatio: {
type: Number,
default: 0.8
},
// 保存的模式 None无限制ScaleTo500(缩放到指定的尺寸 如 500宽度) Max500(不超过500宽度)
saveMode: {
type: String,
default: "None"
}
},
data() {
return {
//data
stageLeft: 0,
stageTop: 0,
stageWidth: 0,
stageHeight: 0,
boxWidth: 0,
boxHeight: 0,
boxLeft: 0,
boxTop: 0,
canvasWidth: 0,
canvasHeight: 0,
show: false,
imagePath: ''
};
},
methods: {
resetCrop() {
this.$emit('reset');
this.init(this.imagePath);
},
cancelCrop() {
this.$emit('cancel');
},
completeCrop() {
let imagePath = this.imagePath;
let canvasContext = uni.createCanvasContext('canvas', this);
let boxLeft = this.boxLeft;
let boxTop = this.boxTop;
let boxWidth = this.boxWidth;
let boxHeight = this.boxHeight;
let sx = Math.ceil(boxLeft * imageStageRatio);
let sy = Math.ceil(boxTop * imageStageRatio);
let sWidth = Math.ceil(boxWidth * imageStageRatio);
let sHeight = Math.ceil(boxHeight * imageStageRatio);
let dx = 0;
let dy = 0;
let dWidth = Math.ceil(sWidth * pixelRatio);
let dHeight = Math.ceil(sHeight * pixelRatio);
const param = {
x: sx,
y: sy,
width: dWidth,
height: dHeight,
rotate: 0,
scaleX: 1,
scaleY: 1
};
let _this = this
canvasContext.drawImage(imagePath, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
canvasContext.draw(false, () => {
let targetWidth = 0
let targetHeight = 0
let mode = _this.saveMode
if (_this.IsEmpty(mode) || mode == "None") {
targetWidth = sWidth
targetHeight = sHeight
} else {
if (mode.startsWith("ScaleTo")) {
let w = Number(mode.substring(7, mode.length))
if (isNaN(w)) {
targetWidth = sWidth
targetHeight = sHeight
} else {
// 缩放到指定
targetWidth = w
targetHeight = sHeight * w / sWidth
}
} else if (mode.startsWith("Max")) {
let w = Number(mode.substring(3, mode.length))
if (isNaN(w)) {
targetWidth = sWidth
targetHeight = sHeight
} else {
if (sWidth > w) {
// 超出缩小
targetWidth = w
targetHeight = sHeight * w / sWidth
} else {
targetWidth = sWidth
targetHeight = sHeight
}
}
} else {
targetWidth = sWidth
targetHeight = sHeight
}
}
uni.canvasToTempFilePath({
x: dx,
y: dy,
width: dWidth,
height: dHeight,
destWidth: targetWidth,
destHeight: targetHeight,
canvasId: 'canvas',
fileType: this.outputFileType,
quality: this.quality,
success: res => {
this.$emit('complete', {
param,
path: res.tempFilePath,
source: this.imagePath
});
}
},
this
);
});
},
touchMove(e) {
let targetId = e.target.id;
let touch = e.touches[0];
let pageX = touch.pageX;
let pageY = touch.pageY;
let offsetX = pageX - touchStartX;
let offsetY = pageY - touchStartY;
if (targetId == 'box') {
let newBoxLeft = touchStartBoxLeft + offsetX;
let newBoxTop = touchStartBoxTop + offsetY;
if (newBoxLeft < 0) {
newBoxLeft = 0;
}
if (newBoxTop < 0) {
newBoxTop = 0;
}
if (newBoxLeft + touchStartBoxWidth > stageWidth) {
newBoxLeft = stageWidth - touchStartBoxWidth;
}
if (newBoxTop + touchStartBoxHeight > stageHeight) {
newBoxTop = stageHeight - touchStartBoxHeight;
}
this.boxLeft = newBoxLeft;
this.boxTop = newBoxTop;
} else if (targetId == 'lt') {
if (this.aspectRatio) {
offsetY = offsetX / this.aspectRatio;
}
let newBoxLeft = touchStartBoxLeft + offsetX;
let newBoxTop = touchStartBoxTop + offsetY;
if (newBoxLeft < 0) {
newBoxLeft = 0;
}
if (newBoxTop < 0) {
newBoxTop = 0;
}
if (touchStartBoxLeft + touchStartBoxWidth - newBoxLeft < minBoxWidth) {
newBoxLeft = touchStartBoxLeft + touchStartBoxWidth - minBoxWidth;
}
if (touchStartBoxTop + touchStartBoxHeight - newBoxTop < minBoxHeight) {
newBoxTop = touchStartBoxTop + touchStartBoxHeight - minBoxHeight;
}
let newBoxWidth = touchStartBoxWidth - (newBoxLeft - touchStartBoxLeft);
let newBoxHeight = touchStartBoxHeight - (newBoxTop - touchStartBoxTop);
//约束比例
if (newBoxTop == 0 && this.aspectRatio && newBoxLeft != 0) {
newBoxWidth = newBoxHeight * this.aspectRatio;
newBoxLeft = touchStartBoxWidth - newBoxWidth + touchStartBoxLeft;
}
if (newBoxLeft == 0 && this.aspectRatio) {
newBoxHeight = newBoxWidth / this.aspectRatio;
newBoxTop = touchStartBoxHeight - newBoxHeight + touchStartBoxTop;
}
if (newBoxWidth == minBoxWidth && this.aspectRatio) {
newBoxHeight = newBoxWidth / this.aspectRatio;
newBoxTop = touchStartBoxHeight - newBoxHeight + touchStartBoxTop;
}
this.boxTop = newBoxTop;
this.boxLeft = newBoxLeft;
this.boxWidth = newBoxWidth;
this.boxHeight = newBoxHeight;
} else if (targetId == 'rt') {
if (this.aspectRatio) {
offsetY = -offsetX / this.aspectRatio;
}
let newBoxWidth = touchStartBoxWidth + offsetX;
if (newBoxWidth < minBoxWidth) {
newBoxWidth = minBoxWidth;
}
if (touchStartBoxLeft + newBoxWidth > stageWidth) {
newBoxWidth = stageWidth - touchStartBoxLeft;
}
let newBoxTop = touchStartBoxTop + offsetY;
if (newBoxTop < 0) {
newBoxTop = 0;
}
if (touchStartBoxTop + touchStartBoxHeight - newBoxTop < minBoxHeight) {
newBoxTop = touchStartBoxTop + touchStartBoxHeight - minBoxHeight;
}
let newBoxHeight = touchStartBoxHeight - (newBoxTop - touchStartBoxTop);
//约束比例
if (newBoxTop == 0 && this.aspectRatio && newBoxWidth != stageWidth - touchStartBoxLeft) {
newBoxWidth = newBoxHeight * this.aspectRatio;
}
if (newBoxWidth == stageWidth - touchStartBoxLeft && this.aspectRatio) {
newBoxHeight = newBoxWidth / this.aspectRatio;
newBoxTop = touchStartBoxHeight - newBoxHeight + touchStartBoxTop;
}
if (newBoxWidth == minBoxWidth && this.aspectRatio) {
newBoxHeight = newBoxWidth / this.aspectRatio;
newBoxTop = touchStartBoxHeight - newBoxHeight + touchStartBoxTop;
}
this.boxTop = newBoxTop;
this.boxHeight = newBoxHeight;
this.boxWidth = newBoxWidth;
} else if (targetId == 'lb') {
if (this.aspectRatio) {
offsetY = -offsetX / this.aspectRatio;
}
let newBoxLeft = touchStartBoxLeft + offsetX;
if (newBoxLeft < 0) {
newBoxLeft = 0;
}
if (touchStartBoxLeft + touchStartBoxWidth - newBoxLeft < minBoxWidth) {
newBoxLeft = touchStartBoxLeft + touchStartBoxWidth - minBoxWidth;
}
let newBoxWidth = touchStartBoxWidth - (newBoxLeft - touchStartBoxLeft);
let newBoxHeight = touchStartBoxHeight + offsetY;
if (newBoxHeight < minBoxHeight) {
newBoxHeight = minBoxHeight;
}
if (touchStartBoxTop + newBoxHeight > stageHeight) {
newBoxHeight = stageHeight - touchStartBoxTop;
}
//约束比例
if (newBoxHeight == stageHeight - touchStartBoxTop && this.aspectRatio && newBoxLeft != 0) {
newBoxWidth = newBoxHeight * this.aspectRatio;
newBoxLeft = touchStartBoxWidth - newBoxWidth + touchStartBoxLeft;
}
if (newBoxLeft == 0 && this.aspectRatio) {
newBoxHeight = newBoxWidth / this.aspectRatio;
}
if (newBoxWidth == minBoxWidth && this.aspectRatio) {
newBoxHeight = newBoxWidth / this.aspectRatio;
}
this.boxLeft = newBoxLeft;
this.boxWidth = newBoxWidth;
this.boxHeight = newBoxHeight;
} else if (targetId == 'rb') {
if (this.aspectRatio) {
offsetY = offsetX / this.aspectRatio;
}
let newBoxWidth = touchStartBoxWidth + offsetX;
if (newBoxWidth < minBoxWidth) {
newBoxWidth = minBoxWidth;
}
if (touchStartBoxLeft + newBoxWidth > stageWidth) {
newBoxWidth = stageWidth - touchStartBoxLeft;
}
let newBoxHeight = touchStartBoxHeight + offsetY;
if (newBoxHeight < minBoxHeight) {
newBoxHeight = minBoxHeight;
}
if (touchStartBoxTop + newBoxHeight > stageHeight) {
newBoxHeight = stageHeight - touchStartBoxTop;
}
//约束比例
if (newBoxHeight == stageHeight - touchStartBoxTop && this.aspectRatio && newBoxWidth != stageWidth -
touchStartBoxLeft) {
newBoxWidth = newBoxHeight * this.aspectRatio;
}
if (newBoxWidth == stageWidth - touchStartBoxLeft && this.aspectRatio) {
newBoxHeight = newBoxWidth / this.aspectRatio;
}
if (newBoxWidth == minBoxWidth && this.aspectRatio) {
newBoxHeight = newBoxWidth / this.aspectRatio;
}
this.boxWidth = newBoxWidth;
this.boxHeight = newBoxHeight;
}
},
touchStart(e) {
let touch = e.touches[0];
let pageX = touch.pageX;
let pageY = touch.pageY;
touchStartX = pageX;
touchStartY = pageY;
touchStartBoxLeft = this.boxLeft;
touchStartBoxTop = this.boxTop;
touchStartBoxWidth = this.boxWidth;
touchStartBoxHeight = this.boxHeight;
},
close(force = true) {
this.show = false;
if (force) {
this.imagePath = ''
}
},
init(src) {
let _this = this
if (!src) {
return '';
}
this.imagePath = src;
uni.showLoading({
mask: true,
title: '载入图片中'
});
uni.createSelectorQuery()
.in(this)
.select('.cropper-body')
.boundingClientRect(rect => {
layoutLeft = rect.left;
layoutTop = rect.top;
layoutWidth = rect.width;
layoutHeight = rect.height;
uni.getImageInfo({
src: _this.imagePath,
success: imageInfo => {
imageWidth = imageInfo.width;
imageHeight = imageInfo.height;
let imageWH = imageWidth / imageHeight;
let layoutWH = layoutWidth / layoutHeight;
if (imageWH >= layoutWH) {
stageWidth = layoutWidth;
stageHeight = stageWidth / imageWH;
imageStageRatio = imageHeight / stageHeight;
} else {
stageHeight = layoutHeight;
stageWidth = layoutHeight * imageWH;
imageStageRatio = imageWidth / stageWidth;
}
stageLeft = (layoutWidth - stageWidth) / 2;
stageTop = (layoutHeight - stageHeight) / 2;
minBoxWidth = stageWidth * _this.minBoxWidthRatio;
minBoxHeight = stageHeight * _this.minBoxHeightRatio;
let boxWidth = stageWidth * _this.initialBoxWidthRatio;
let boxHeight = stageHeight * _this.initialBoxHeightRatio;
if (_this.aspectRatio) {
boxHeight = boxWidth / _this.aspectRatio;
}
if (boxHeight > stageHeight) {
boxHeight = stageHeight;
boxWidth = boxHeight * _this.aspectRatio;
}
let boxLeft = (stageWidth - boxWidth) / 2;
let boxTop = (stageHeight - boxHeight) / 2;
_this.canvasWidth = imageWidth * pixelRatio;
_this.canvasHeight = imageHeight * pixelRatio;
_this.stageLeft = stageLeft;
_this.stageTop = stageTop;
_this.stageWidth = stageWidth;
_this.stageHeight = stageHeight;
_this.boxWidth = boxWidth;
_this.boxHeight = boxHeight;
_this.boxLeft = boxLeft;
_this.boxTop = boxTop;
setTimeout(() => {
uni.hideLoading();
_this.show = true;
}, 100);
},
fail: () => {
uni.showToast({
icon: 'none',
title: '图片载入失败'
});
}
});
})
.exec();
}
}
};
</script>
<style lang="scss">
.cropper {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: #000;
z-index: -1000000;
opacity: 0;
&.show {
z-index: 9901;
opacity: 1;
}
.cropper-head {
position: fixed;
top: 0;
width: 750rpx;
z-index: 6;
height: calc(var(--status-bar-height) + 88rpx);
padding-top: var(--status-bar-height);
display: flex;
justify-content: flex-start;
align-items: center;
}
.cropper-btn {
height: 64rpx;
margin: 0 20rpx;
padding: 0 30rpx;
line-height: 64rpx;
color: #fff;
font-size: 26rpx;
}
.cropper-body {
margin: calc(var(--status-bar-height) + 88rpx) 30rpx 0 30rpx;
height: calc(100vh - var(--status-bar-height) - 88rpx - 100rpx - var(--safe-area-inset-bottom));
position: relative;
}
.cropper-bottom {
height: calc(var(--safe-area-inset-bottom) + 100rpx);
padding-top: var(--safe-area-inset-bottom);
display: flex;
align-items: center;
justify-content: space-between;
position: fixed;
z-index: 6;
width: 750rpx;
bottom: 0;
}
.cropper-ok {
color: #39f;
}
// 此属性导致Android显示异常
.cropper-image {
position: absolute;
width: 100%;
height: 100%;
}
.cropper-stage {
position: absolute;
.cropper-box {
position: absolute;
border: 4rpx solid #ddd;
box-sizing: border-box;
box-shadow: 0 0 0 2000rpx rgba(0, 0, 0, 0.5);
.lt {
position: absolute;
height: 48rpx;
width: 48rpx;
left: -6rpx;
top: -6rpx;
border-left: 12rpx solid #ffffff;
border-top: 12rpx solid #ffffff;
}
.lb {
position: absolute;
height: 48rpx;
width: 48rpx;
left: -6rpx;
bottom: -6rpx;
border-left: 12rpx solid #ffffff;
border-bottom: 12rpx solid #ffffff;
}
.rt {
position: absolute;
height: 48rpx;
width: 48rpx;
right: -6rpx;
top: -6rpx;
border-right: 12rpx solid #ffffff;
border-top: 12rpx solid #ffffff;
}
.rb {
position: absolute;
height: 48rpx;
width: 48rpx;
right: -6rpx;
bottom: -6rpx;
border-right: 12rpx solid #ffffff;
border-bottom: 12rpx solid #ffffff;
}
.line-v,
.line-h {
position: absolute;
opacity: 0.5;
}
.line-v {
width: 2rpx;
border-left: 2rpx dashed #fff;
height: 100%;
}
.line-h {
height: 2rpx;
border-bottom: 2rpx dashed #fff;
width: 100%;
}
}
}
.cropper-canvas {
position: fixed;
background-color: red;
left: 5000rpx;
}
}
// 安全域兼容样式
page {
--safe-area-inset-top: 0px;
--safe-area-inset-right: 0px;
--safe-area-inset-bottom: 0px;
--safe-area-inset-left: 0px;
@supports (top: constant(safe-area-inset-top)) {
--safe-area-inset-top: constant(safe-area-inset-top);
--safe-area-inset-right: constant(safe-area-inset-right);
--safe-area-inset-bottom: constant(safe-area-inset-bottom);
--safe-area-inset-left: constant(safe-area-inset-left);
}
@supports (top: env(safe-area-inset-top)) {
--safe-area-inset-top: env(safe-area-inset-top);
--safe-area-inset-right: env(safe-area-inset-right);
//--safe-area-inset-bottom: 12px;
--safe-area-inset-bottom: env(safe-area-inset-bottom);
--safe-area-inset-left: env(safe-area-inset-left);
}
}
</style>

View File

@@ -0,0 +1,79 @@
#### 【注意】由于赶时间,只做了功能性测试,兼容性测试暂时无法进行,更不用说优化测试了。欢迎有遇到问题的留言,或者自己动手修改也可以。
#### 【注意】因为属于业余时间开发的一个项目所依赖的插件,所以更新较缓慢。
### 使用方式
```javascript
import Cropper from '@/components/cropper/cropper.vue';
```
```html
<cropper :imagePath="originalFaceSrc" :aspectRatio="1" @complete="complete" @cancel="cancel" v-if="handleFaceStatus"></cropper>
```
使用v-if控制显示mounted内执行init方法使用简单操作便利。
## 参数
| 参数 | 类型 | 解释 | 默认 | 必填 | 备注 |
| ------------ | ------------ | ------------ | ------------ | ------------ | ------------ |
| quality | Number | 返回图片质量 | 1 | 否 | 如返回的图片仅需预览不用上传,请适当调低 |
| imagePath | String | 原始图片路径 |"" | 是 | blob、静态图片资源地址或者base64未测试 |
| outputFileType | String | 目标文件的类型 | jpg | 否 | jpg输出jpg格式图片png输出png格式图片 |
| aspectRatio | Number, null | 目标图片的宽高比 | null | 否 | null即不限制剪裁宽高比。aspectRatio需大于0 |
| minBoxWidthRatio | Number | 最小剪裁尺寸与原图尺寸的比率(宽)| 0.15 | 否 | 宽度最小剪裁到原图的0.15宽 |
| minBoxHeightRatio | Number | 最小剪裁尺寸与原图尺寸的比率(高)| 0.15 | 否 | 当设置aspectRatio时minBoxHeight值设置无效。minBoxHeight值由minBoxWidth 和 aspectRatio自动计算得到 |
| initialBoxWidthRatio | Number | 剪裁框初始大小比率(宽) | 0.8 | 否 | 裁框默认宽度为图片宽度的0.8倍 |
| initialBoxHeightRatio| Number | 剪裁框初始大小比率(高) | 0.8 | 否 | 当设置aspectRatio时initialBoxHeightRatio值设置无效。initialBoxHeightRatio值由initialBoxWidthRatio 和 aspectRatio自动计算得到|
| saveMode | String | 保存模式 | "None" | 否 | None无限制ScaleTo500(缩放到指定的尺寸 如 500宽度) Max500(不超过500宽度) |
## 事件
| 事件名 | 解释 | 参数 | 备注 |
| ------------ | ------------ | ------------ | ------------ |
| reset | 用户点击重做后触发 | 无 | 可能无用,但留一个给开发者备用 |
| cancel | 用户点击取消后触发 | 无 | 开发者可以将组件置为消失v-if尽量不要用v-show |
| complete | 用户点击裁剪后触发 | path路径h5为base64app与小程序为临时路径param裁切参数见【备注1】 | 核心功能 |
### 备注1
param
```json
x:Number, //裁切左上角X坐标
y:Number, //裁切左上角Y坐标
width:Number, //裁切宽度
height:Number, //裁切高度
rotate:Number, //旋转(未实现,预留)
scaleX:Number, //X轴缩放未实现预留
scaleY:Number, //Y轴缩放未实现预留
```
### 备注2
本组件样式支持iphone安全范围但需要手动开启
请在插件被样式中最后备注的地方解除备注即可。
控制台会报错,无视即可。
或者复制如下样式样式代码
```css
// 安全域兼容样式
page {
--safe-area-inset-top: 0px;
--safe-area-inset-right: 0px;
--safe-area-inset-bottom: 0px;
--safe-area-inset-left: 0px;
@supports (top: constant(safe-area-inset-top)) {
--safe-area-inset-top: constant(safe-area-inset-top);
--safe-area-inset-right: constant(safe-area-inset-right);
--safe-area-inset-bottom: constant(safe-area-inset-bottom);
--safe-area-inset-left: constant(safe-area-inset-left);
}
@supports (top: env(safe-area-inset-top)) {
--safe-area-inset-top: env(safe-area-inset-top);
--safe-area-inset-right: env(safe-area-inset-right);
//--safe-area-inset-bottom: 12px;
--safe-area-inset-bottom: env(safe-area-inset-bottom);
--safe-area-inset-left: env(safe-area-inset-left);
}
}
```

View File

@@ -0,0 +1,163 @@
<template>
<view :style="{'height':navHeightPx,'display':'flex','flex-direction':'row','align-items':'center'}"
v-if="btnType!=0">
<view class="uni-combox__input-box">
<image v-if="btnType==2 && filterCandidatesLength > 0" :style="{'height':navHeightPx,'width':navHeightPx}"
mode="aspectFit" :src="btnSource" @click.stop="toggleSelector">
</image>
<text v-if="btnType==1 && filterCandidatesLength > 0"
:style="{'height':navHeightPx,'line-height':navHeightPx,'font-size':'28rpx','color':'#FFFFFF','padding-left':'20rpx','padding-right':'20rpx'}"
@click.stop="toggleSelector">{{btnSource}}</text>
<view>
<view class="uni-combox__selector" v-if="showSelector" :style="{'margin-left': offsetPx}">
<scroll-view scroll-y="true" class="uni-combox__selector-scroll">
<view class="uni-combox__selector-empty" v-if="filterCandidatesLength === 0">
<text>{{emptyTips}}</text>
</view>
<view class="uni-combox__selector-item" v-for="(item,index) in candidates" :key="index"
@click="onSelectorClick(index)">
<image :src="item.src" class="modal-img"></image>
<text
style="display:-webkit-box;-webkit-box-orient:vertical; -webkit-line-clamp:1; overflow:hidden;">{{item.name}}</text>
</view>
</scroll-view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'uni-combox',
props: {
btnType: {
type: String,
// 0不使用 1使用文字 2使用图片
default: "0"
},
btnSource: {
type: String,
default: ""
},
emptyTips: {
type: String,
default: '无'
},
candidates: {
type: Array,
default () {
return []
}
}
},
data() {
return {
showSelector: false,
navHeightPx: "",
navWidth: "",
windowWidth: "",
offsetPx: ""
}
},
computed: {
filterCandidatesLength() {
return this.candidates.length;
}
},
methods: {
toggleSelector() {
this.showSelector = !this.showSelector
},
onSelectorClick(index) {
this.showSelector = false
this.$emit('onselect', index, this.candidates)
},
closeDropItem() {
this.showSelector = false
}
},
created() {
this.navHeightPx = getApp().globalData.navHeightPx
this.navWidth = getApp().globalData.navWidth
this.windowWidth = getApp().globalData.windowWidth
let offset = this.windowWidth - this.navWidth;
this.offsetPx = "-160rpx"
// 获取胶囊高度
// #ifdef MP-WEIXIN||MP-BAID||MP-QQ||MP-TOUTIAO
this.offsetPx = "-" + (this.windowWidth - this.navWidth - 20) + "px"
// #endif
}
}
</script>
<style lang="scss" scoped>
.uni-combox__input-box {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
}
.uni-combox__selector {
box-sizing: border-box;
position: absolute;
top: 42px;
left: 0;
width: 100%;
background-color: #FFFFFF;
border-radius: 6px;
box-shadow: #DDDDDD 4px 4px 8px, #DDDDDD -4px -4px 8px;
z-index: 2;
width: 240rpx;
}
.uni-combox__selector-scroll {
max-height: 200px;
box-sizing: border-box;
}
.uni-combox__selector::before {
content: '';
position: absolute;
width: 0;
height: 0;
border-bottom: solid 6px #FFFFFF;
border-right: solid 6px transparent;
border-left: solid 6px transparent;
left: 85%;
top: -6px;
margin-left: -6px;
}
.uni-combox__selector-empty,
.uni-combox__selector-item {
display: flex;
line-height: 80rpx;
font-size: 14px;
text-align: center;
border-bottom: solid 1px #DDDDDD;
margin: 0px 10px;
flex-direction: row;
align-items: center;
justify-content: center;
}
.uni-combox__selector-empty:last-child,
.uni-combox__selector-item:last-child {
border-bottom: none;
}
.modal-img {
width: 40rpx;
height: 40rpx;
margin-right: 5rpx;
}
</style>

BIN
components/firstui/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,379 @@
<template>
<view class="fui-button__wrap"
:style="{width: width,height: height,marginTop:margin[0] || 0, marginRight:margin[1]||0,marginBottom:margin[2] || margin[0]||0,marginLeft:margin[3] || margin[1]||0,borderRadius: radius}"
@touchstart="handleStart" @touchend="handleClick" @touchcancel="handleEnd" @tap="handleTap">
<button class="fui-button" :class="[
bold ? 'fui-text__bold' : '',
time && (plain || type==='link') ? 'fui-button__opacity' : '',
disabled && !disabledBackground ? 'fui-button__opacity' : '',
!background && !disabledBackground && !plain?('fui-button__'+type):'',
!width || width==='100%' || width==='true'?'fui-button__flex-1':'',
time && !plain && type!=='link' ? 'fui-button__active' : ''
]" :style="{
width: width,
height: height,
lineHeight: height,
background: disabled && disabledBackground ? disabledBackground : (plain ? 'transparent' : background),
borderWidth:borderWidth,
borderColor: borderColor ? borderColor : disabled && disabledBackground ? disabledBackground : (background || 'transparent'),
borderRadius: radius,
fontSize: size + 'rpx',
color: disabled && disabledBackground ? disabledColor : color
}" :loading="loading" :form-type="formType" :open-type="openType" @getuserinfo="bindgetuserinfo"
@getphonenumber="bindgetphonenumber" @contact="bindcontact" @error="binderror"
@opensetting="bindopensetting" :disabled="disabled" :scope="scope">
<text class="fui-button__text"
:class="{'fui-btn__gray-color':!background && !disabledBackground && !plain && type==='gray' && color==='#fff','fui-text__bold':bold}"
v-if="text"
:style="{fontSize: size + 'rpx',lineHeight:size + 'rpx',color: disabled && disabledBackground ? disabledColor : color}">{{text}}</text>
<slot></slot>
</button>
</view>
</template>
<script>
export default {
name: 'fui-button',
emits: ['click', 'getuserinfo', 'contact', 'getphonenumber', 'error', 'opensetting'],
// #ifndef VUE3
// #ifdef MP-WEIXIN
behaviors: ['wx://form-field-button'],
// #endif
// #endif
props: {
//样式类型primarysuccess warningdangerlinkpurplegray
type: {
type: String,
default: 'primary'
},
//按钮背景色当传入值时type失效
background: {
type: String,
default: ''
},
//按钮显示文本
text: {
type: String,
default: ''
},
//按钮字体颜色
color: {
type: String,
default: '#fff'
},
//按钮禁用背景色
disabledBackground: {
type: String,
default: ''
},
//按钮禁用字体颜色
disabledColor: {
type: String,
default: ''
},
borderWidth: {
type: String,
// #ifdef APP-NVUE
default: '0.5px'
// #endif
// #ifndef APP-NVUE
default: '1rpx'
// #endif
},
borderColor: {
type: String,
default: ''
},
//宽度
width: {
type: String,
default: '100%'
},
//高度
height: {
type: String,
default: '96rpx'
},
//字体大小单位rpx
size: {
type: [Number, String],
default: 32
},
bold: {
type: Boolean,
default: false
},
//['20rpx','30rpx','20rpx','30rpx']->[上,右,下,左]
margin: {
type: Array,
default () {
return ['0', '0']
}
},
//圆角
radius: {
type: String,
default: '16rpx'
},
plain: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
formType: {
type: String,
default: ''
},
openType: {
type: String,
default: ''
},
//支付宝小程序
//当 open-type 为 getAuthorize 时,可以设置 scope 为phoneNumber、userInfo
scope: {
type: String,
default: ''
},
index: {
type: [Number, String],
default: 0
}
},
data() {
return {
time: 0,
trigger: false,
tap: false
};
},
methods: {
handleStart() {
if (this.disabled) return;
this.trigger = false;
this.tap = true;
if (new Date().getTime() - this.time <= 150) return;
this.trigger = true;
this.time = new Date().getTime();
},
handleClick() {
if (this.disabled || !this.trigger) return;
//如果想取消操作,比如长按再放开,这里可以做时间判断
this.time = 0;
this.$emit('click', {
index: Number(this.index)
});
},
handleTap() {
// #ifdef H5
if (this.disabled || this.tap) return;
this.$emit('click', {
index: Number(this.index)
});
// #endif
},
handleEnd() {
if (this.disabled) return;
setTimeout(() => {
this.time = 0;
}, 150);
},
bindgetuserinfo({
detail = {}
} = {}) {
this.$emit('getuserinfo', detail);
},
bindcontact({
detail = {}
} = {}) {
this.$emit('contact', detail);
},
bindgetphonenumber({
detail = {}
} = {}) {
this.$emit('getphonenumber', detail);
},
binderror({
detail = {}
} = {}) {
this.$emit('error', detail);
},
bindopensetting({
detail = {}
} = {}) {
this.$emit('opensetting', detail);
}
}
};
</script>
<style scoped>
.fui-button__wrap {
position: relative;
}
.fui-button {
/* #ifdef APP-NVUE */
border-width: 0.5px;
/* #endif */
/* #ifndef APP-NVUE */
border-width: 1rpx;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
/* #endif */
border-style: solid;
position: relative;
padding-left: 0;
padding-right: 0;
overflow: hidden;
/* #ifndef APP-NVUE */
transform: translateZ(0);
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
/* #endif */
}
.fui-button__flex-1 {
flex: 1;
/* #ifndef APP-NVUE */
width: 100%;
/* #endif */
}
.fui-button::after {
border: 0;
}
/* #ifndef APP-NVUE */
.fui-button__active {
overflow: hidden !important;
}
.fui-button__active::after {
content: ' ';
background-color: var(--fui-bg-color-hover, rgba(0, 0, 0, 0.2));
position: absolute;
width: 100%;
height: 100%;
left: 0;
right: 0;
top: 0;
transform: none;
z-index: 1;
border-radius: 0;
}
/* #endif */
.fui-button__text {
text-align: center;
flex-direction: row;
align-items: center;
justify-content: center !important;
padding-left: 0 !important;
}
.fui-button__opacity {
opacity: 0.5;
}
.fui-text__bold {
font-weight: bold;
}
.fui-button__link {
border-color: transparent !important;
background: transparent !important;
}
.fui-button__primary {
/* #ifdef APP-NVUE */
border-color: #465CFF !important;
background: #465CFF !important;
/* #endif */
/* #ifndef APP-NVUE */
border-color: var(--fui-color-primary, #465CFF) !important;
background: var(--fui-color-primary, #465CFF) !important;
/* #endif */
}
.fui-button__success {
/* #ifdef APP-NVUE */
border-color: #09BE4F !important;
background: #09BE4F !important;
/* #endif */
/* #ifndef APP-NVUE */
border-color: var(--fui-color-success, #09BE4F) !important;
background: var(--fui-color-success, #09BE4F) !important;
/* #endif */
}
.fui-button__warning {
/* #ifdef APP-NVUE */
border-color: #FFB703 !important;
background: #FFB703 !important;
/* #endif */
/* #ifndef APP-NVUE */
border-color: var(--fui-color-warning, #FFB703) !important;
background: var(--fui-color-warning, #FFB703) !important;
/* #endif */
}
.fui-button__danger {
/* #ifdef APP-NVUE */
border-color: #FF2B2B !important;
background: #FF2B2B !important;
/* #endif */
/* #ifndef APP-NVUE */
border-color: var(--fui-color-danger, #FF2B2B) !important;
background: var(--fui-color-danger, #FF2B2B) !important;
/* #endif */
}
.fui-button__purple {
/* #ifdef APP-NVUE */
border-color: #6831FF !important;
background: #6831FF !important;
/* #endif */
/* #ifndef APP-NVUE */
border-color: var(--fui-color-purple, #6831FF) !important;
background: var(--fui-color-purple, #6831FF) !important;
/* #endif */
}
.fui-button__gray {
/* #ifdef APP-NVUE */
border-color: #F8F8F8 !important;
background: #F8F8F8 !important;
/* #endif */
/* #ifndef APP-NVUE */
border-color: var(--fui-bg-color-content, #F8F8F8) !important;
background: var(--fui-bg-color-content, #F8F8F8) !important;
color: var(--fui-color-primary, #465CFF) !important;
/* #endif */
}
.fui-btn__gray-color {
/* #ifdef APP-NVUE */
color: #465CFF !important;
/* #endif */
/* #ifndef APP-NVUE */
color: var(--fui-color-primary, #465CFF) !important;
/* #endif */
}
</style>

View File

@@ -0,0 +1,445 @@
<template>
<view class="fui-dropdown__menu">
<slot></slot>
<view class="fui-dropdown__menu-list"
:class="{'fui-ddm__down':direction!=='up','fui-ddm__up':direction==='up','fui-ddm__down-show':isShow && direction!=='up','fui-ddm__up-show':isShow && direction==='up'}"
:style="getStyles">
<scroll-view class="fui-ddm__scroll" scroll-y :style="{maxHeight:maxHeight+'rpx',minWidth:minWidth+'rpx'}">
<view class="fui-dropdown__menu-item" :style="{background:background,padding:padding}"
:class="{'fui-ddm__reverse':isReverse,'fui-ddm__item-line':splitLine && itemList.length-1!==index}"
v-for="(model,index) in itemList" :key="index" @tap.stop="itemClick(index)">
<view class="fui-ddm__checkbox"
:class="{'fui-is__checkmark':isCheckMark,'fui-ddm__checkbox-color':(!checkboxColor || checkboxColor=='true') && model.checked && !isCheckMark}"
:style="{background:model.checked && !isCheckMark ?checkboxColor:'transparent',borderColor:model.checked && !isCheckMark ?checkboxColor:borderColor}"
v-if="isCheckbox">
<view class="fui-ddm__checkmark"
:style="{borderBottomColor:checkmarkColor,borderRightColor:checkmarkColor}"
v-if="model.checked"></view>
</view>
<view class="fui-ddm__flex">
<view class="fui-ddm__icon-box"
:class="{'fui-ddm__icon-ml':!isReverse && isCheckbox,'fui-ddm__icon-mr':isReverse}"
:style="{width:iconWidth+'rpx',height:iconWidth+'rpx'}" v-if="model.src">
<image src="/static/images/common/logo.png"
:style="{width:iconWidth+'rpx',height:iconWidth+'rpx'}"></image>
</view>
<text class="fui-ddm__item-text"
:class="{'fui-ddm__text-pl':!isReverse && (isCheckbox || model.src),'fui-ddm__text-pr':isReverse && (isCheckbox || model.src)}"
:style="{fontSize:size+'rpx',color:selectedColor && model.checked?selectedColor:color}">{{model.text}}</text>
</view>
</view>
</scroll-view>
</view>
<view class="fui-ddm__mask" :style="{backgrround:maskBackground}" v-if="isShow && isMask" @tap="close(1)">
</view>
</view>
</template>
<script>
/*!
* 下拉菜单
* 由于weex android上overflow仅支持hidden所以该组件不支持Nvue端
* Nvue端使用组件fui-dropdown-list或fui-select组件替代该组件
*/
export default {
name: "fui-dropdown-menu",
emits: ['click', 'close'],
props: {
options: {
type: Array,
default () {
return []
}
},
maxHeight: {
type: [Number, String],
default: 400
},
minWidth: {
type: [Number, String],
default: 0
},
left: {
type: [Number, String],
default: 0
},
//right大于等于0时生效left失效
right: {
type: [Number, String],
default: -1
},
background: {
type: String,
default: '#fff'
},
radius: {
type: [Number, String],
default: 0
},
padding: {
type: String,
default: '32rpx'
},
isCheckbox: {
type: Boolean,
default: true
},
//选择框选中后颜色
checkboxColor: {
type: String,
// #ifdef APP-NVUE
default: '#465CFF'
// #endif
// #ifndef APP-NVUE
default: ''
// #endif
},
//选择框未选中时边框颜色
borderColor: {
type: String,
default: '#ccc'
},
//是否只展示对号,无边框背景
isCheckMark: {
type: Boolean,
default: false
},
//对号颜色
checkmarkColor: {
type: String,
default: '#fff'
},
//选择框与内容是否颠倒排列
isReverse: {
type: Boolean,
default: false
},
//是否需要分割线条
splitLine: {
type: Boolean,
default: false
},
iconWidth: {
type: [Number, String],
default: 48
},
size: {
type: [Number, String],
default: 32
},
color: {
type: String,
default: '#181818'
},
selectedColor: {
type: String,
default: ''
},
isMask: {
type: Boolean,
default: true
},
maskBackground: {
type: String,
default: 'transparent'
},
//down/up
direction: {
type: String,
default: 'down'
}
},
watch: {
options(newVal) {
this.initData(newVal)
}
},
data() {
return {
itemList: [],
isShow: false
};
},
computed: {
getStyles() {
let styles = `border-radius:${this.radius}rpx;background:${this.background};`
let right = Number(this.right || 0)
if (right >= 0) {
styles += 'right:0;'
} else {
styles += 'left:0;'
}
return styles
}
},
created() {
this.initData(this.options)
},
methods: {
initData(vals) {
if (vals && vals.length > 0) {
if (typeof vals[0] !== 'object') {
vals = vals.map(item => {
return {
text: item,
checked: false
}
})
} else {
vals.map(item => {
item.checked = item.checked || false
})
}
this.itemList = vals;
}
},
itemClick(index) {
let item = this.itemList[index]
let vals = [...this.itemList]
vals.forEach((item, idx) => {
if (index === idx) {
item.checked = true
} else {
item.checked = false
}
})
this.itemList = vals;
this.$emit('click', {
index: index,
...item
})
this.close(2)
},
close(type) {
this.isShow = false;
if (type === 1) {
this.$emit('close', {})
}
},
show() {
this.isShow = true;
}
}
}
</script>
<style scoped>
.fui-dropdown__menu {
flex: 1;
position: relative;
}
.fui-ddm__scroll {
/* #ifndef APP-NVUE */
width: auto;
/* #endif */
}
.fui-dropdown__menu-list {
position: absolute;
box-shadow: 0 0 10rpx rgba(2, 4, 38, 0.05);
overflow: hidden;
z-index: 992;
opacity: 0;
/* #ifndef APP-NVUE */
visibility: hidden;
transition: all 0.3s ease-in-out;
/* #endif */
}
.fui-ddm__down {
transform-origin: 0 0;
bottom: 0;
/* #ifndef APP-NVUE */
transform: translate3d(0, 100%, 0) scaleY(0);
/* #endif */
}
.fui-ddm__down-show {
/* #ifndef APP-NVUE */
transform: translate3d(0, 100%, 0) scaleY(1);
visibility: visible;
/* #endif */
opacity: 1;
}
.fui-ddm__up {
transform-origin: 0 100%;
top: 0;
/* #ifndef APP-NVUE */
transform: translate3d(0, -100%, 0) scaleY(0);
/* #endif */
}
.fui-ddm__up-show {
/* #ifndef APP-NVUE */
transform: translate3d(0, -100%, 0) scaleY(1);
visibility: visible;
/* #endif */
opacity: 1;
}
.fui-ddm__mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 990;
}
.fui-dropdown__menu-item {
/* #ifndef APP-NVUE */
width: 100%;
display: flex;
box-sizing: border-box;
transform: scale(1) translateZ(0);
/* #endif */
flex: 1;
flex-direction: row;
align-items: center;
background-color: #FFFFFF;
position: relative;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.fui-ddm__flex {
/* #ifndef APP-NVUE */
width: 100%;
display: flex;
box-sizing: border-box;
/* #endif */
flex: 1;
flex-direction: row;
align-items: center;
}
/* #ifndef APP-NVUE */
.fui-ddm__item-line {
position: relative;
}
.fui-ddm__item-line::after {
content: '';
position: absolute;
border-bottom: 1px solid var(--fui-color-border, #EEEEEE);
/* #ifdef H5 */
transform: scaleY(0.5);
/* #endif */
/* #ifndef H5 */
transform: scaleY(0.5) translateZ(0);
/* #endif */
transform-origin: 0 100%;
bottom: 0;
right: 0;
left: 32rpx;
pointer-events: none;
}
/* #endif */
.fui-dropdown__menu-item:active {
/* #ifdef APP-NVUE */
background-color: rgba(0, 0, 0, .2) !important;
/* #endif */
/* #ifndef APP-NVUE */
background-color: var(--fui-bg-color-hover, rgba(0, 0, 0, .2)) !important;
/* #endif */
}
.fui-ddm__reverse {
justify-content: space-between;
flex-direction: row-reverse;
}
.fui-ddm__checkbox {
font-size: 0;
color: rgba(0, 0, 0, 0);
width: 40rpx;
height: 40rpx;
border-width: 1px;
border-style: solid;
/* #ifdef APP-NVUE */
border-radius: 40rpx;
/* #endif */
/* #ifndef APP-NVUE */
display: inline-flex;
box-sizing: border-box;
border-radius: 50%;
vertical-align: top;
flex-shrink: 0;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
/* #ifndef APP-NVUE */
.fui-ddm__checkbox-color {
background: var(--fui-color-primary, #465CFF) !important;
border-color: var(--fui-color-primary, #465CFF) !important;
}
/* #endif */
.fui-is__checkmark {
border-width: 0 !important;
background: transparent !important;
}
.fui-ddm__checkmark {
width: 20rpx;
height: 40rpx;
border-bottom-style: solid;
border-bottom-width: 3px;
border-bottom-color: #FFFFFF;
border-right-style: solid;
border-right-width: 3px;
border-right-color: #FFFFFF;
/* #ifndef APP-NVUE */
box-sizing: border-box;
transform: rotate(45deg) scale(0.5) translateZ(0);
/* #endif */
/* #ifdef APP-NVUE */
transform: rotate(45deg) scale(0.5);
/* #endif */
transform-origin: 54% 48%;
}
.fui-ddm__item-text {
/* #ifndef APP-NVUE */
word-break: break-all;
/* #endif */
font-weight: normal;
}
.fui-ddm__text-pl {
padding-left: 24rpx;
}
.fui-ddm__text-pr {
padding-right: 24rpx;
}
.fui-ddm__icon-box {
overflow: hidden;
background-color: #F1F4FA;
/* #ifndef APP-NVUE */
flex-shrink: 0;
/* #endif */
}
.fui-ddm__icon-ml {
margin-left: 24rpx;
}
.fui-ddm__icon-mr {
margin-right: 24rpx;
}
</style>

View File

@@ -0,0 +1,154 @@
export default {
"addressbook":"\ue80c",
"addfriends-fill": "\ue80a",
"addfriends": "\ue80b",
"backspace-fill": "\ue808",
"backspace": "\ue809",
"bankcard-fill": "\ue806",
"bankcard": "\ue807",
"camera-fill": "\ue804",
"camera": "\ue805",
"captcha-fill": "\ue802",
"captcha": "\ue803",
"cart-fill": "\ue800",
"cart": "\ue801",
"classify": "\ue7fe",
"classify-fill": "\ue7ff",
"comment-fill": "\ue7fc",
"comment": "\ue7fd",
"community-fill": "\ue7fa",
"community": "\ue7fb",
"coupon-fill": "\ue7f8",
"coupon": "\ue7f9",
"delete": "\ue7f6",
"delete-fill": "\ue7f7",
"edit": "\ue7f4",
"edit-fill": "\ue7f5",
"fabulous-fill": "\ue7f2",
"fabulous": "\ue7f3",
"find": "\ue7f0",
"find-fill": "\ue7f1",
"help-fill": "\ue7ee",
"help": "\ue7ef",
"home-fill": "\ue7ec",
"home": "\ue7ed",
"idcard-fill": "\ue7ea",
"idcard": "\ue7eb",
"info": "\ue7e8",
"info-fill": "\ue7e9",
"invite-fill": "\ue7e6",
"invite": "\ue7e7",
"kefu-fill": "\ue7e4",
"kefu": "\ue7e5",
"like-fill": "\ue7e2",
"like": "\ue7e3",
"location": "\ue7e0",
"location-fill": "\ue7e1",
"lock": "\ue7de",
"lock-fill": "\ue7df",
"mail-fill": "\ue7dc",
"mail": "\ue7dd",
"message": "\ue7da",
"message-fill": "\ue7db",
"mobile-fill": "\ue7d8",
"mobile": "\ue7d9",
"more": "\ue7d6",
"more-fill": "\ue7d7",
"my-fill": "\ue7d4",
"my": "\ue7d5",
"principal":"\ue80d",
"notice-fill": "\ue7d2",
"notice": "\ue7d3",
"order": "\ue7d0",
"order-fill": "\ue7d1",
"picture": "\ue7ce",
"picture-fill": "\ue7cf",
"setup-fill": "\ue7cc",
"setup": "\ue7cd",
"share": "\ue7ca",
"share-fill": "\ue7cb",
"shop": "\ue7c8",
"shop-fill": "\ue7c9",
"star-fill": "\ue7c5",
"star": "\ue7c6",
"starhalf": "\ue7c7",
"stepon-fill": "\ue7c3",
"stepon": "\ue7c4",
"wait-fill": "\ue7c1",
"wait": "\ue7c2",
"warning": "\ue7bf",
"warning-fill": "\ue7c0",
"plus": "\ue7bc",
"plussign-fill": "\ue7bd",
"plussign": "\ue7be",
"minus": "\ue7b9",
"minussign": "\ue7ba",
"minussign-fill": "\ue7bb",
"close": "\ue7b8",
"clear": "\ue7b6",
"clear-fill": "\ue7b7",
"checkbox-fill": "\ue7b5",
"checkround": "\ue7b4",
"checkbox": "\ue7b3",
"check": "\ue7b2",
"pulldown-fill": "\ue7ae",
"pullup": "\ue7af",
"pullup-fill": "\ue7b0",
"pulldown": "\ue7b1",
"roundright-fill": "\ue7ac",
"roundright": "\ue7ad",
"arrowright": "\ue7a9",
"arrowleft": "\ue7aa",
"arrowdown": "\ue7ab",
"left": "\ue7a6",
"up": "\ue7a7",
"right": "\ue7a8",
"back": "\ue7a3",
"top": "\ue7a4",
"dropdown": "\ue7a5",
"turningleft": "\ue79f",
"turningup": "\ue7a0",
"turningright": "\ue7a1",
"turningdown": "\ue7a2",
"refresh": "\ue79c",
"loading": "\ue79d",
"search": "\ue79e",
"rotate": "\ue79b",
"screen": "\ue79a",
"signin": "\ue799",
"calendar": "\ue798",
"scan": "\ue797",
"qrcode": "\ue796",
"wallet": "\ue795",
"telephone": "\ue794",
"visible": "\ue793",
"invisible": "\ue792",
"menu": "\ue78e",
"operate": "\ue78f",
"slide": "\ue790",
"list": "\ue791",
"nonetwork": "\ue78d",
"partake": "\ue78c",
"qa": "\ue78b",
"barchart": "\ue788",
"piechart": "\ue789",
"linechart": "\ue78a",
"at": "\ue787",
"face": "\ue77f",
"redpacket": "\ue780",
"suspend": "\ue781",
"link": "\ue782",
"keyboard": "\ue783",
"play": "\ue784",
"video": "\ue785",
"voice": "\ue786",
"sina": "\ue77a",
"browser": "\ue77b",
"moments": "\ue77c",
"qq": "\ue77d",
"wechat": "\ue77e",
"balance": "\ue779",
"bankcardpay": "\ue778",
"wxpay": "\ue777",
"alipay": "\ue776"
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,217 @@
<template>
<view class="fui-list__cell" :class="{'fui-highlight':highlight,'fui-list__cell-background':!background}"
:style="{paddingTop:padding[0] || 0,paddingRight:padding[1] || 0,paddingBottom:padding[2] || padding[0] || 0,paddingLeft:padding[3] || padding[1] || 0,background:background,marginTop:marginTop+'rpx',marginBottom:marginBottom+'rpx',borderRadius:radius}"
@tap="handleClick">
<view v-if="topBorder" :style="{background:borderColor,left:topLeft+'rpx',right:topRight+'rpx'}"
class="fui-cell__border-top" :class="{'fui-cell__border-color':!borderColor}"></view>
<slot></slot>
<view v-if="bottomBorder" :style="{background:borderColor,left:bottomLeft+'rpx',right:bottomRight+'rpx'}"
class="fui-cell__border-bottom" :class="{'fui-cell__border-color':!borderColor}"></view>
<view class="fui-cell__arrow" v-if="arrow" :style="{'border-color':arrowColor}">
</view>
</view>
</template>
<script>
export default {
name: "fui-list-cell",
emits: ['click'],
props: {
//padding值上、右、下、左,nvue下padding-right(右)无效
padding: {
type: Array,
default () {
return ['32rpx', '32rpx']
}
},
//margin-top 单位rpx
marginTop: {
type: [Number, String],
default: 0
},
//margin-bottom 单位rpx
marginBottom: {
type: [Number, String],
default: 0
},
//背景颜色
// #ifdef APP-NVUE
background: {
type: String,
default: '#fff'
},
// #endif
// #ifndef APP-NVUE
background: {
type: String,
default: ''
},
// #endif
//是否有点击效果
highlight: {
type: Boolean,
default: true
},
//是否需要右侧箭头
arrow: {
type: Boolean,
default: false
},
arrowColor: {
type: String,
default: '#B2B2B2'
},
//是否显示上边框
topBorder: {
type: Boolean,
default: false
},
//是否显示下边框
bottomBorder: {
type: Boolean,
default: true
},
//边框颜色非nvue下传值则全局默认样式失效
// #ifdef APP-NVUE
borderColor: {
type: String,
default: '#EEEEEE'
},
// #endif
// #ifndef APP-NVUE
borderColor: {
type: String,
default: ''
},
// #endif
//上边框left值单位rpx
topLeft: {
type: [Number, String],
default: 0
},
//上边框right值单位rpx
topRight: {
type: [Number, String],
default: 0
},
//下边框left值单位rpx
bottomLeft: {
type: [Number, String],
default: 32
},
//下边框right值单位rpx
bottomRight: {
type: [Number, String],
default: 0
},
//border-radius圆角值
radius: {
type: String,
default: '0'
},
index: {
type: Number,
default: 0
}
},
methods: {
handleClick() {
this.$emit('click', {
index: this.index
});
}
}
}
</script>
<style scoped>
.fui-list__cell {
position: relative;
flex: 1;
/* #ifndef APP-NVUE */
width: 100%;
display: flex;
box-sizing: border-box;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: space-between;
}
/* #ifdef APP-NVUE */
.fui-list__item {
flex: 1;
}
/* #endif */
.fui-cell__arrow {
height: 40rpx;
width: 40rpx;
border-width: 3px 3px 0 0;
border-style: solid;
transform: rotate(45deg) scale(0.5);
/* #ifndef APP-NVUE */
border-radius: 4rpx;
flex-shrink: 0;
margin-left: auto;
box-sizing: border-box;
/* #endif */
/* #ifdef APP-NVUE */
border-top-right-radius: 3rpx;
/* #endif */
transform-origin: center center;
margin-right: -5.8579rpx;
}
.fui-cell__border-top {
position: absolute;
top: 0;
/* #ifdef APP-NVUE */
height: 0.5px;
z-index: -1;
/* #endif */
/* #ifndef APP-NVUE */
height: 1px;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
transform-origin: 0 0;
z-index: 1;
/* #endif */
}
.fui-cell__border-bottom {
position: absolute;
bottom: 0;
/* #ifdef APP-NVUE */
height: 0.5px;
z-index: -1;
/* #endif */
/* #ifndef APP-NVUE */
height: 1px;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
transform-origin: 0 100%;
z-index: 1;
/* #endif */
}
/* #ifndef APP-NVUE */
.fui-cell__border-color {
background-color: var(--fui-color-border, #EEEEEE) !important;
}
.fui-list__cell-background {
background-color: var(--fui-bg-color, #fff);
}
/* #endif */
.fui-highlight:active {
/* #ifdef APP-NVUE */
background-color: rgba(0, 0, 0, 0.2) !important;
/* #endif */
/* #ifndef APP-NVUE */
background-color: var(--fui-bg-color-hover, rgba(0, 0, 0, 0.2)) !important;
/* #endif */
}
</style>

View File

@@ -0,0 +1,85 @@
/*
FirstUI组件内置的基础变量
1.如果你是组件使用者,你可以通过修改这些变量的值来定制自己的组件主题,实现自定义主题功能
2.如果全局修改需要在项目根目录下App.vue文件中引入此css文件
3.如果组件中有props属性是针对颜色设置默认为空值则优先级props变量如果有传值> 全局主题色
*/
/* #ifndef APP-NVUE */
page {
/* 行为相关颜色 */
--fui-color-primary: #465CFF;
--fui-color-success: #09BE4F;
--fui-color-warning: #FFB703;
--fui-color-danger: #FF2B2B;
--fui-color-purple: #6831FF;
/* 文字基本颜色、其他辅助色 */
/* 用于重量级文字信息、标题 */
--fui-color-title: #181818;
/* 用于普通级段落信息、引导词 */
--fui-color-section: #333333;
/* 用于次要标题内容 */
--fui-color-subtitle: #7F7F7F;
/* 用于底部标签、描述、次要文字信息 */
--fui-color-label: #B2B2B2;
/* 用于辅助、次要信息、禁用文字等。如:待输入状态描述文字,已点击按钮文字 */
--fui-color-minor: #CCCCCC;
--fui-color-white: #FFFFFF;
/* 链接颜色 */
--fui-color-link: #465CFF;
/* 背景颜色 */
--fui-bg-color: #ffffff;
/* 页面背景底色 */
--fui-bg-color-grey: #F1F4FA;
/* 内容模块底色 */
--fui-bg-color-content: #F8F8F8;
--fui-bg-color-red: rgba(255, 43, 43, .05);
--fui-bg-color-yellow: rgba(255, 183, 3, .1);
--fui-bg-color-purple: rgba(104, 49, 255, .05);
--fui-bg-color-green: rgba(9, 190, 79, .05);
/* 点击背景色 */
--fui-bg-color-hover: rgba(0, 0, 0, 0.2);
/* 遮罩颜色 */
--fui-bg-color-mask: rgba(0, 0, 0, 0.6);
/* 边框颜色 */
--fui-color-border: #EEEEEE;
/* 阴影颜色 */
--fui-color-shadow: rgba(2, 4, 38, 0.05);
/*禁用态的透明度 */
--fui-opacity-disabled: 0.5;
/* 图标尺寸 */
--fui-img-size-sm: 48rpx;
--fui-img-size-base: 56rpx;
--fui-img-size-middle: 64rpx;
--fui-img-size-lg: 96rpx;
/* 图片尺寸 */
--fui-img-sm: 60rpx;
--fui-img-base: 120rpx;
--fui-img-lg: 240rpx;
/* Border Radius */
--fui-border-radius-sm: 16rpx;
--fui-border-radius-base: 24rpx;
--fui-border-radius-lg: 48rpx;
--fui-border-radius-circle: 50%;
/* 水平间距 */
--fui-spacing-row-sm: 16rpx;
--fui-spacing-row-base: 24rpx;
--fui-spacing-row-lg: 32rpx;
/* 垂直间距 */
--fui-spacing-col-sm: 8rpx;
--fui-spacing-col-base: 16rpx;
--fui-spacing-col-lg: 24rpx;
}
/* #endif */

21
components/grid/grid.vue Normal file
View File

@@ -0,0 +1,21 @@
<template>
<view class="grid-out">
<slot></slot>
</view>
</template>
<script>
export default {
methods: {
}
}
</script>
<style lang="scss">
.grid-out {
display: flex;
align-content: flex-start;
flex-flow: row wrap;
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<view class="async-switch">
<switch :checked="checked" :disabled="disabled" :color="changeColor" />
<button @click="onChange"></button>
</view>
</template>
<script>
export default {
name:'async-switch',
props:{
checked: {
type: Boolean,
default: false
},
disabled:{
type: Boolean,
default: false
},
color:{
type:String,
default:'#007aff'
},
disabledColor:{
type:String,
default:'#333'
}
},
data() {
return {
};
},
computed:{
changeColor(){
return (this.disabled ? this.disabledColor : this.color)
}
},
methods:{
onChange(){
/* 禁用状态不返回事件 */
if(this.disabled){
return;
}
this.$emit("change");
}
}
}
</script>
<style scoped lang="scss">
.async-switch{
display: inline-block;
position: relative;
&>switch{
margin: 0;
}
&>button{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
}
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<view style="display: flex;flex-direction: column;box-sizing: border-box;">
<view v-if="border == 1 || border ==3" class="border"></view>
<view style="width: 100%;height: 30rpx;"></view>
<view style="display: flex;flex-direction: row;align-items: center;margin-left: 16rpx;margin-right: 15rpx;">
<text style="font-size: 26rpx;color: #101010;" :class="{ 'enabled': !enabled }">{{title}}</text>
<image v-if="required" src="../../static/asterisk.png"
style="width: 14rpx;height: 16rpx;margin-left: 12rpx;flex-shrink: 0;"></image>
</view>
<view style="width: 100%;height: 30rpx;"></view>
<view style="margin-left: 16rpx;margin-right: 15rpx;" :class="{ 'enabled': !enabled }">
<!--自定义类型的选择器-->
<!--自定义数据 需提供数据源 以及 取值key-->
<picker v-if="type==0" :disabled="!enabled" :range-key="dataDetailKey" :range="data" @change="custom">
<view style="display: flex;flex-direction: row;align-items: center;width: 100%;">
<view v-if="select == undefined || select == null || select === ''"
style="font-size: 28rpx;color: #BCBCBC;">{{hint}}</view>
<view v-if="select != undefined && select != null || select != ''"
style="font-size: 28rpx;color: #2E2D2D;">{{select}}</view>
<view style="flex: 1;"></view>
<image src="../../static/drop.png"
style="width: 45rpx;height: 45rpx;margin-left: 12rpx;flex-shrink: 0;">
</image>
</view>
</picker>
<!--内置选择器 支持 男女 以及 是否-->
<picker v-if="type==1||type==2" :disabled="!enabled" range-key="detail" :range="type==1?sex:yes"
@change="preset">
<view style="display: flex;flex-direction: row;align-items: center;width: 100%;">
<view v-if="select == undefined || select == null || select === ''"
style="font-size: 28rpx;color: #BCBCBC;">{{hint}}</view>
<view v-if="select != undefined && select != null || select != ''"
style="font-size: 28rpx;color: #2E2D2D;">{{select}}</view>
<view style="flex: 1;"></view>
<image src="../../static/drop.png"
style="width: 45rpx;height: 45rpx;margin-left: 12rpx;flex-shrink: 0;">
</image>
</view>
</picker>
<!--用户自行实现 选择的方式-->
<view v-if="type!=0&&type!=1&&type!=2"
style="display: flex;flex-direction: row;align-items: center;width: 100%;" @click="impl">
<view v-if="select == undefined || select == null || select === ''"
style="font-size: 28rpx;color: #BCBCBC;">{{hint}}</view>
<view v-if="select != undefined && select != null || select != ''"
style="font-size: 28rpx;color: #2E2D2D;">{{select}}</view>
<view style="flex: 1;"></view>
<image src="../../static/drop.png"
style="width: 45rpx;height: 45rpx;margin-left: 12rpx;flex-shrink: 0;">
</image>
</view>
</view>
<view style="width: 100%;height: 25rpx;"></view>
<view v-if="border == 2 || border ==3" class="border"></view>
</view>
</template>
<script>
export default {
name: "item-drop",
props: {
// 是否必填,即是否显示星号
required: {
type: Boolean,
default: false
},
// 标题
title: {
type: String,
default: ""
},
// 提示信息
hint: {
type: String,
default: ""
},
// 显示的内容
content: {
type: String,
default: ""
},
// 分割线
// 0 无上线分割线
// 1 只有上分割线
// 2 只有下分割线
// 3 上下都有分割线
border: {
type: Number,
default: 2
},
// 是否可使用
enabled: {
type: Boolean,
default: true
},
// 下拉类型
// 0自定义数据
// 1男女
// 2是否
// 3自己实现
type: {
type: Number,
default: 0
},
// 弹出数据
data: {
type: Array,
default: []
},
// 弹出数据选择取值key
dataDetailKey: {
type: String,
default: ""
}
},
mounted() {
this.select = this.content
},
data() {
return {
sex: [{
"detail": "男",
"value": "0001"
},
{
"detail": "女",
"value": "0002"
}
],
yes: [{
"detail": "是",
"value": "0001"
},
{
"detail": "否",
"value": "0002"
}
],
select: "",
selector: {}
};
},
methods: {
custom(e) {
let index = e.detail.value
// 选中的
this.selector = this.data[index]
// 选择的值
this.select = this.selector[this.dataDetailKey]
console.log("选择+")
console.log(this.selector)
},
preset(e) {
let index = e.detail.value
// 选中的
this.selector = this.type == 1 ? this.sex[index] : this.yes[index]
// 选择的值
console.log("选择+")
console.log(this.selector)
},
impl() {
if (this.enabled)
// 对方提供监听
this.$emit("select")
}
}
}
</script>
<style lang="scss">
.border {
height: 1rpx;
background-color: #DFDFDF;
}
.enabled {
opacity: 0.5;
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<view style="display: flex;flex-direction: column;box-sizing: border-box;">
<view v-if="border == 1 || border == 3" class="border"></view>
<view style="width: 100%;height: 30rpx;"></view>
<view style="display: flex;flex-direction: row;align-items: center;margin-left: 16rpx;margin-right: 15rpx;">
<text style="font-size: 26rpx;color: #101010;" :class="{ 'enabled': !enabled }">{{title}}</text>
<image v-if="required" src="../../static/asterisk.png"
style="width: 14rpx;height: 16rpx;margin-left: 12rpx;flex-shrink: 0;"></image>
</view>
<view style="width: 100%;height: 30rpx;"></view>
<!--选择类型ocr识别-->
<view v-if="data.length > 0"
style="display: flex;flex-direction: row;align-items: center;margin-left: 16rpx;margin-right: 15rpx;"
:class="{ 'enabled': !enabled }">
<input style="font-size: 28rpx;color: #2E2D2D;flex: 1;" placeholder-style="font-size: 28rpx;color: #BCBCBC;"
:value="input" @input="inputValue" :placeholder="hint" />
<picker :disabled="!enabled" :range-key="dataDetailKey" :range="data" @change="custom">
<image src="../../static/scan.png"
style="width: 45rpx;height: 45rpx;margin-left: 12rpx;flex-shrink: 0;">
</picker>
</image>
</view>
<!--默认身份证类型-->
<view v-if="data.length == 0"
style="display: flex;flex-direction: row;align-items: center;margin-left: 16rpx;margin-right: 15rpx;"
:class="{ 'enabled': !enabled }">
<input style="font-size: 28rpx;color: #2E2D2D;flex: 1;" placeholder-style="font-size: 28rpx;color: #BCBCBC;"
:value="input" @input="inputValue" :placeholder="hint" />
<image src="../../static/scan.png" style="width: 45rpx;height: 45rpx;margin-left: 12rpx;flex-shrink: 0;"
@click="scan">
</image>
</view>
<view style="width: 100%;height: 25rpx;"></view>
<view v-if="border == 2 || border ==3" class="border"></view>
</view>
</template>
<script>
//引入配置
import B64 from '../../commons/LocalFile2Base64.js';
export default {
name: "item-scan",
props: {
// 是否必填,即是否显示星号
required: {
type: Boolean,
default: false
},
// 标题
title: {
type: String,
default: ""
},
// 提示信息
hint: {
type: String,
default: ""
},
// 显示的内容
content: {
type: String,
default: ""
},
// 分割线
// 0 无上线分割线
// 1 只有上分割线
// 2 只有下分割线
// 3 上下都有分割线
border: {
type: Number,
default: 2
},
// 是否可使用
enabled: {
type: Boolean,
default: true
},
// 弹出数据
data: {
type: Array,
default () {
return []
}
},
// 弹出数据选择取值key识别类型的
dataDetailKey: {
type: String,
default: ""
}
},
created() {
this.input = this.content
},
data() {
return {
input: "",
select: ""
};
},
methods: {
scan() {
if (this.enabled) {
let _this = this;
uni.chooseImage({
count: 1,
sizeType: ['original'],
success: function(res) {
B64(res.tempFilePaths[0])
.then((res) => {
// 成功
console.log(res)
}, (err) => {
// 失败
_this.Toast(err)
})
}
})
}
},
inputValue(e) {
console.log(e.detail.value)
},
custom(e) {
let index = e.detail.value
// ocr类型
this.select = this.data[index][this.dataDetailKey]
this.scan()
}
}
}
</script>
<style lang="scss">
.border {
height: 1rpx;
background-color: #DFDFDF;
}
.enabled {
opacity: 0.5;
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<view>
<view style="width: 100%;margin-top: 10rpx;display: flex;justify-content: center;">
<image src="../../static/custom-icon/jiazai.png" mode="aspectFill" class="loading spin"></image>
</view>
</view>
</template>
<script>
export default {
data() {
return {};
},
methods: {
}
}
</script>
<style>
.loading {
margin-top: 10rpx;
width: 20px;
height: 20px;
margin-bottom: 20rpx;
}
.spin {
animation: spin 1s steps(8) infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,56 @@
/* 下拉刷新区域 */
.mescroll-downwarp {
position: absolute;
top: -100%;
left: 0;
width: 100%;
height: 100%;
text-align: center;
}
/* 下拉刷新--内容区,定位于区域底部 */
.mescroll-downwarp .downwarp-content {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
min-height: 60rpx;
padding: 20rpx 0;
text-align: center;
}
/* 下拉刷新--提示文本 */
.mescroll-downwarp .downwarp-tip {
display: inline-block;
font-size: 28rpx;
vertical-align: middle;
margin-left: 16rpx;
/* color: gray; 已在style设置color,此处删去*/
}
/* 下拉刷新--旋转进度条 */
.mescroll-downwarp .downwarp-progress {
display: inline-block;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid gray;
border-bottom-color: transparent !important;
/*已在style设置border-color,此处需加 !important*/
vertical-align: middle;
}
/* 旋转动画 */
.mescroll-downwarp .mescroll-rotate {
animation: mescrollDownRotate 0.6s linear infinite;
}
@keyframes mescrollDownRotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,54 @@
<!-- 下拉刷新区域 -->
<template>
<view v-if="mOption.use" class="mescroll-downwarp"
:style="{'background-color':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
<view class="downwarp-content">
<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}"
:style="{'border-color':mescroll.optDown.textColor, 'transform':downRotate}"></view>
<view class="downwarp-tip">{{downText}}</view>
</view>
</view>
</template>
<script>
export default {
props: {
option: Object, // down的配置项
type: Number, // 下拉状态inOffset1 outOffset2 showLoading3 endDownScroll4
rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
},
computed: {
// 支付宝小程序需写成计算属性,prop定义default仍报错
mOption() {
return this.option || {}
},
// 是否在加载中
isDownLoading() {
return this.type === 3
},
// 旋转的角度
downRotate() {
return 'rotate(' + 360 * this.rate + 'deg)'
},
// 文本提示
downText() {
switch (this.type) {
case 1:
return this.mOption.textInOffset;
case 2:
return this.mOption.textOutOffset;
case 3:
return this.mOption.textLoading;
case 4:
return this.mOption.textLoading;
default:
return this.mOption.textInOffset;
}
}
}
};
</script>
<style>
@import "./mescroll-down.css";
</style>

View File

@@ -0,0 +1,92 @@
<!--空布局
可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理:
import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue';
<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
-->
<template>
<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }"
:style="{ 'z-index': option.zIndex, top: option.top }">
<image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" />
<view v-if="tip" class="empty-tip">{{ tip }}</view>
<view v-if="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view>
</view>
</template>
<script>
// 引入全局配置
import GlobalOption from './../mescroll-uni-option.js';
export default {
props: {
// empty的配置项: 默认为GlobalOption.up.empty
option: {
type: Object,
default () {
return {};
}
}
},
// 使用computed获取配置,用于支持option的动态配置
computed: {
// 图标
icon() {
return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 此处不使用短路求值, 用于支持传空串不显示图标
},
// 文本提示
tip() {
return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 此处不使用短路求值, 用于支持传空串不显示文本提示
}
},
methods: {
// 点击按钮
emptyClick() {
this.$emit('emptyclick');
}
}
};
</script>
<style>
/* 无任何数据的空布局 */
.mescroll-empty {
box-sizing: border-box;
width: 100%;
padding: 100rpx 50rpx;
text-align: center;
}
.mescroll-empty.empty-fixed {
z-index: 99;
position: absolute;
/*transform会使fixed失效,最终会降级为absolute */
top: 100rpx;
left: 0;
}
.mescroll-empty .empty-icon {
width: 280rpx;
height: 280rpx;
}
.mescroll-empty .empty-tip {
margin-top: 20rpx;
font-size: 24rpx;
color: gray;
}
.mescroll-empty .empty-btn {
display: inline-block;
margin-top: 40rpx;
min-width: 200rpx;
padding: 18rpx;
font-size: 28rpx;
border: 1rpx solid #e04b28;
border-radius: 60rpx;
color: #e04b28;
}
.mescroll-empty .empty-btn:active {
opacity: 0.75;
}
</style>

View File

@@ -0,0 +1,80 @@
<!-- 回到顶部的按钮 -->
<template>
<image class="mescroll-totop"
:class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-safe-bottom': mOption.safearea}]"
:style="{'z-index':mOption.zIndex, 'left': left, 'right': right, 'bottom':addUnit(mOption.bottom), 'width':addUnit(mOption.width), 'border-radius':addUnit(mOption.radius)}"
src="/static/img//uniapp/mescrollTotop.png" mode="widthFix" @click="toTopClick" />
</template>
<script>
export default {
props: {
// up.toTop的配置项
option: Object,
// 是否显示
value: false
},
computed: {
// 支付宝小程序需写成计算属性,prop定义default仍报错
mOption() {
return this.option || {}
},
// 优先显示左边
left() {
return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto';
},
// 右边距离 (优先显示左边)
right() {
return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right);
}
},
methods: {
addUnit(num) {
if (!num) return 0;
if (typeof num === 'number') return num + 'rpx';
return num
},
toTopClick() {
this.$emit('input', false); // 使v-model生效
this.$emit('click'); // 派发点击事件
}
}
};
</script>
<style>
/* 回到顶部的按钮 */
.mescroll-totop {
z-index: 9990;
position: fixed !important;
/* 加上important避免编译到H5,在多mescroll中定位失效 */
right: 20rpx;
bottom: 120rpx;
width: 72rpx;
height: auto;
border-radius: 50%;
opacity: 0;
transition: opacity 0.5s;
/* 过渡 */
margin-bottom: var(--window-bottom);
/* css变量 */
}
/* 适配 iPhoneX */
.mescroll-safe-bottom {
margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom));
/* window-bottom + 适配 iPhoneX */
margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
}
/* 显示 -- 淡入 */
.mescroll-totop-in {
opacity: 1;
}
/* 隐藏 -- 淡出且不接收事件*/
.mescroll-totop-out {
opacity: 0;
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,47 @@
/* 上拉加载区域 */
.mescroll-upwarp {
min-height: 60rpx;
padding: 30rpx 0;
text-align: center;
clear: both;
}
/*提示文本 */
.mescroll-upwarp .upwarp-tip,
.mescroll-upwarp .upwarp-nodata {
display: inline-block;
font-size: 28rpx;
vertical-align: middle;
/* color: gray; 已在style设置color,此处删去*/
}
.mescroll-upwarp .upwarp-tip {
margin-left: 16rpx;
}
/*旋转进度条 */
.mescroll-upwarp .upwarp-progress {
display: inline-block;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid gray;
border-bottom-color: transparent !important;
/*已在style设置border-color,此处需加 !important*/
vertical-align: middle;
}
/* 旋转动画 */
.mescroll-upwarp .mescroll-rotate {
animation: mescrollUpRotate 0.6s linear infinite;
}
@keyframes mescrollUpRotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,39 @@
<!-- 上拉加载区域 -->
<template>
<view class="mescroll-upwarp" :style="{'background-color':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-if="isUpLoading">
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
<view class="upwarp-tip">{{ mOption.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
</view>
</template>
<script>
export default {
props: {
option: Object, // up的配置项
type: Number // 上拉加载的状态0loading前1loading中2没有更多了
},
computed: {
// 支付宝小程序需写成计算属性,prop定义default仍报错
mOption() {
return this.option || {};
},
// 加载中
isUpLoading() {
return this.type === 1;
},
// 没有更多了
isUpNoMore() {
return this.type === 2;
}
}
};
</script>
<style>
@import './mescroll-up.css';
</style>

View File

@@ -0,0 +1,15 @@
page {
-webkit-overflow-scrolling: touch;
/* 使iOS滚动流畅 */
}
.mescroll-body {
position: relative;
/* 下拉刷新区域相对自身定位 */
height: auto;
/* 不可固定高度,否则overflow: hidden, 可通过设置最小高度使列表不满屏仍可下拉*/
overflow: hidden;
/* 遮住顶部下拉刷新区域 */
box-sizing: border-box;
/* 避免设置padding出现双滚动条的问题 */
}

View File

@@ -0,0 +1,309 @@
<template>
<view class="mescroll-body"
:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom, 'padding-bottom': padBottomConstant, 'padding-bottom': padBottomEnv }"
@touchstart="touchstartEvent" @touchmove="touchmoveEvent" @touchend="touchendEvent"
@touchcancel="touchendEvent">
<view class="mescroll-body-content" :style="{ transform: translateY, transition: transition }">
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
<view v-if="mescroll.optDown.use" class="mescroll-downwarp"
:style="{'background-color':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
<view class="downwarp-content">
<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}"
:style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
<view class="downwarp-tip">{{downText}}</view>
</view>
</view>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading" class="mescroll-upwarp"
:style="{'background-color':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-if="upLoadType===1">
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}">
</view>
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
</view>
</view>
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
</view>
</template>
<script>
// 引入mescroll-uni.js,处理核心逻辑
import MeScroll from './mescroll-uni.js';
// 引入全局配置
import GlobalOption from './mescroll-uni-option.js';
// 引入空布局组件
import MescrollEmpty from './components/mescroll-empty.vue';
// 引入回到顶部组件
import MescrollTop from './components/mescroll-top.vue';
export default {
components: {
MescrollEmpty,
MescrollTop
},
data() {
return {
mescroll: {
optDown: {},
optUp: {}
}, // mescroll实例
downHight: 0, //下拉刷新: 容器高度
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
downLoadType: 4, // 下拉刷新状态 inOffset1 outOffset2 showLoading3 endDownScroll4
upLoadType: 0, // 上拉加载状态0loading前1loading中2没有更多了
isShowEmpty: false, // 是否显示空布局
isShowToTop: false, // 是否显示回到顶部按钮
windowHeight: 0, // 可使用窗口的高度
statusBarHeight: 0, // 状态栏高度
isSafearea: false // 支持安全区
};
},
props: {
down: Object, // 下拉刷新的参数配置
up: Object, // 上拉加载的参数配置
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
topbar: Boolean, // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可自动加上状态栏高度的偏移量)
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
height: [String, Number] // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
},
computed: {
// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
minHeight() {
return this.toPx(this.height || '100%') + 'px'
},
// 下拉布局往下偏移的距离 (px)
numTop() {
return this.toPx(this.top) + (this.topbar ? this.statusBarHeight : 0);
},
padTop() {
return this.numTop + 'px';
},
// 上拉布局往上偏移 (px)
numBottom() {
return this.toPx(this.bottom);
},
padBottom() {
return this.numBottom + 'px';
},
padBottomConstant() {
return this.isSafearea ? 'calc(' + this.padBottom + ' + constant(safe-area-inset-bottom))' : this
.padBottom;
},
padBottomEnv() {
return this.isSafearea ? 'calc(' + this.padBottom + ' + env(safe-area-inset-bottom))' : this.padBottom;
},
// 是否为重置下拉的状态
isDownReset() {
return this.downLoadType === 3 || this.downLoadType === 4;
},
// 过渡
transition() {
return this.isDownReset ? 'transform 300ms' : this.downTransition;
},
translateY() {
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' :
''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
},
// 是否在加载中
isDownLoading() {
return this.downLoadType === 3
},
// 旋转的角度
downRotate() {
return 'rotate(' + 360 * this.downRate + 'deg)'
},
// 文本提示
downText() {
switch (this.downLoadType) {
case 1:
return this.mescroll.optDown.textInOffset;
case 2:
return this.mescroll.optDown.textOutOffset;
case 3:
return this.mescroll.optDown.textLoading;
case 4:
return this.mescroll.optDown.textLoading;
default:
return this.mescroll.optDown.textInOffset;
}
}
},
methods: {
//number,rpx,upx,px,% --> px的数值
toPx(num) {
if (typeof num === 'string') {
if (num.indexOf('px') !== -1) {
if (num.indexOf('rpx') !== -1) {
// "10rpx"
num = num.replace('rpx', '');
} else if (num.indexOf('upx') !== -1) {
// "10upx"
num = num.replace('upx', '');
} else {
// "10px"
return Number(num.replace('px', ''));
}
} else if (num.indexOf('%') !== -1) {
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
let rate = Number(num.replace('%', '')) / 100;
return this.windowHeight * rate;
}
}
return num ? uni.upx2px(Number(num)) : 0;
},
//注册列表touchstart事件,用于下拉刷新
touchstartEvent(e) {
this.mescroll.touchstartEvent(e);
},
//注册列表touchmove事件,用于下拉刷新
touchmoveEvent(e) {
this.mescroll.touchmoveEvent(e);
},
//注册列表touchend事件,用于下拉刷新
touchendEvent(e) {
this.mescroll.touchendEvent(e);
},
// 点击空布局的按钮回调
emptyClick() {
this.$emit('emptyclick', this.mescroll);
},
// 点击回到顶部的按钮回调
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
}
},
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
created() {
let vm = this;
let diyOption = {
// 下拉刷新的配置
down: {
inOffset(mescroll) {
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
},
outOffset(mescroll) {
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
},
onMoving(mescroll, rate, downHight) {
// 下拉过程中的回调,滑动过程一直在执行;
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
},
showLoading(mescroll, downHight) {
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
endDownScroll(mescroll) {
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
// 派发下拉刷新的回调
callback: function(mescroll) {
vm.$emit('down', mescroll);
}
},
// 上拉加载的配置
up: {
// 显示加载中的回调
showLoading() {
vm.upLoadType = 1;
},
// 显示无更多数据的回调
showNoMore() {
vm.upLoadType = 2;
},
// 隐藏上拉加载的回调
hideUpScroll() {
vm.upLoadType = 0;
},
// 空布局
empty: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowEmpty = isShow;
}
},
// 回到顶部
toTop: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowToTop = isShow;
}
},
// 派发上拉加载的回调
callback: function(mescroll) {
vm.$emit('up', mescroll);
}
}
};
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
let myOption = JSON.parse(
JSON.stringify({
down: vm.down,
up: vm.up
})
); // 深拷贝,避免对props的影响
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
// 初始化MeScroll对象
vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
// init回调mescroll对象
vm.$emit('init', vm.mescroll);
// 设置高度
const sys = uni.getSystemInfoSync();
if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
// 使down的bottomOffset生效
vm.mescroll.setBodyHeight(sys.windowHeight);
// mescroll-body在Android小程序下拉会卡顿,无法像mescroll-uni那样通过设置"disableScroll":true解决,只能用动画过渡缓解
// #ifdef MP
if (sys.platform == "android") vm.downTransition = 'transform 200ms'
// #endif
// 因为使用的是page的scroll,这里需自定义scrollTo
vm.mescroll.resetScrollTo((y, t) => {
uni.pageScrollTo({
scrollTop: y,
duration: t
})
});
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
if (sys.platform == "ios") {
vm.isSafearea = vm.safearea;
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
} else {
vm.isSafearea = false
vm.mescroll.optUp.toTop.safearea = false
}
}
};
</script>
<style>
@import "./mescroll-body.css";
@import "./components/mescroll-down.css";
@import './components/mescroll-up.css';
</style>

View File

@@ -0,0 +1,60 @@
// mescroll-body 和 mescroll-uni 通用
// import MescrollUni from "./mescroll-uni.vue";
// import MescrollBody from "./mescroll-body.vue";
const MescrollMixin = {
// components: { // 非H5端无法通过mixin注册组件, 只能在main.js中注册全局组件或具体界面中注册
// MescrollUni,
// MescrollBody
// },
data() {
return {
mescroll: null //mescroll实例对象
}
},
// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
onPullDownRefresh() {
this.mescroll && this.mescroll.onPullDownRefresh();
},
// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
onPageScroll(e) {
this.mescroll && this.mescroll.onPageScroll(e);
},
// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
onReachBottom() {
this.mescroll && this.mescroll.onReachBottom();
},
methods: {
// mescroll组件初始化的回调,可获取到mescroll对象
mescrollInit(mescroll) {
this.mescroll = mescroll;
this.mescrollInitByRef(); // 兼容字节跳动小程序
},
// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序: http://www.mescroll.com/qa.html?v=20200107#q26)
mescrollInitByRef() {
if (!this.mescroll || !this.mescroll.resetUpScroll) {
let mescrollRef = this.$refs.mescrollRef;
if (mescrollRef) this.mescroll = mescrollRef.mescroll
}
},
// 下拉刷新的回调
downCallback() {
// mixin默认resetUpScroll
this.mescroll.resetUpScroll()
},
// 上拉加载的回调
upCallback() {
// mixin默认延时500自动结束加载
setTimeout(() => {
this.mescroll.endErr();
}, 500)
}
},
mounted() {
this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
}
}
export default MescrollMixin;

View File

@@ -0,0 +1,34 @@
// 全局配置
// mescroll-body 和 mescroll-uni 通用
const GlobalOption = {
down: {
// 其他down的配置参数也可以写,这里只展示了常用的配置:
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
textLoading: '加载中 ...', // 加载中的提示文本
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
},
up: {
// 其他up的配置参数也可以写,这里只展示了常用的配置:
textLoading: '加载中 ...', // 加载中的提示文本
textNoMore: '-- END --', // 没有更多数据的提示文本
offset: 80, // 距底部多远时,触发upCallback
isBounce: false, // 默认禁止橡皮筋的回弹效果, 必读事项: http://www.mescroll.com/qa.html?v=190725#q25
toTop: {
// 回到顶部按钮,需配置src才显示
src: "/static/img/uniapp/mescrollTotop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
},
empty: {
use: true, // 是否显示空布局
icon: "/static/img/uniapp/zanwushuju.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
tip: '' // 提示
}
}
}
export default GlobalOption

View File

@@ -0,0 +1,33 @@
page {
height: 100%;
box-sizing: border-box;
/* 避免设置padding出现双滚动条的问题 */
}
.mescroll-uni-warp {
height: 100%;
}
.mescroll-uni {
position: relative;
width: 100%;
height: 100%;
min-height: 200rpx;
overflow-y: auto;
box-sizing: border-box;
/* 避免设置padding出现双滚动条的问题 */
}
/* 定位的方式固定高度 */
.mescroll-uni-fixed {
z-index: 1;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: auto;
/* 使right生效 */
height: auto;
/* 使bottom生效 */
}

View File

@@ -0,0 +1,869 @@
/* mescroll
* version 1.2.5
* 2020-03-15 wenju
* http://www.mescroll.com
*/
export default function MeScroll(options, isScrollBody) {
let me = this;
me.version = '1.2.5'; // mescroll版本号
me.options = options || {}; // 配置
me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
me.isDownScrolling = false; // 是否在执行下拉刷新的回调
me.isUpScrolling = false; // 是否在执行上拉加载的回调
let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
// 初始化下拉刷新
me.initDownScroll();
// 初始化上拉加载,则初始化
me.initUpScroll();
// 自动加载
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
// 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
if (me.optDown.use && me.optDown.auto && hasDownCallback) {
if (me.optDown.autoShowLoading) {
me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
} else {
me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
}
}
// 自动触发上拉加载
setTimeout(function() { // 延时确保先执行down的callback,再执行up的callback,因为部分小程序emit是异步,会导致isUpAutoLoad判断有误
me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
}, 100)
}, 30); // 需让me.optDown.inited和me.optUp.inited先执行
}
/* 配置参数:下拉刷新 */
MeScroll.prototype.extendDownScroll = function(optDown) {
// 下拉刷新的配置
MeScroll.extend(optDown, {
use: true, // 是否启用下拉刷新; 默认true
auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
isLock: false, // 是否锁定下拉刷新,默认false;
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
startTop: 100, // scroll-view滚动到顶部时,此时的scroll-top不一定为0, 此值用于控制最大的误差
fps: 80, // 下拉节流 (值越大每秒刷新频率越高)
inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
minAngle: 45, // 向下滑动最少偏移的角度,取值区间 [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
textLoading: '加载中 ...', // 加载中的提示文本
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
inited: null, // 下拉刷新初始化完毕的回调
inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
outOffset: null, // 下拉的距离大于offset那一刻的回调
onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
showLoading: null, // 显示下拉刷新进度的回调
afterLoading: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
endDownScroll: null, // 结束下拉刷新的回调
callback: function(mescroll) {
// 下拉刷新的回调;默认重置上拉加载列表为第一页
mescroll.resetUpScroll();
}
})
}
/* 配置参数:上拉加载 */
MeScroll.prototype.extendUpScroll = function(optUp) {
// 上拉加载的配置
MeScroll.extend(optUp, {
use: true, // 是否启用上拉加载; 默认true
auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
isLock: false, // 是否锁定上拉加载,默认false;
isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
isBounce: false, // 默认禁止橡皮筋的回弹效果, 必读事项: http://www.mescroll.com/qa.html?v=190725#q25
callback: null, // 上拉加载的回调;function(page,mescroll){ }
page: {
num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
size: 10, // 每页数据的数量
time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
},
noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
offset: 80, // 距底部多远时,触发upCallback
textLoading: '加载中 ...', // 加载中的提示文本
textNoMore: '-- END --', // 没有更多数据的提示文本
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
inited: null, // 初始化完毕的回调
showLoading: null, // 显示加载中的回调
showNoMore: null, // 显示无更多数据的回调
hideUpScroll: null, // 隐藏上拉加载的回调
errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
toTop: {
// 回到顶部按钮,需配置src才显示
src: null, // 图片路径,默认null (绝对路径或网络图)
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
btnClick: null, // 点击按钮的回调
onShow: null, // 是否显示的回调
zIndex: 9990, // fixed定位z-index值
left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
},
empty: {
use: true, // 是否显示空布局
icon: null, // 图标路径
tip: '~ 暂无相关数据 ~', // 提示
btnText: '', // 按钮
btnClick: null, // 点击按钮的回调
onShow: null, // 是否显示的回调
fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
zIndex: 99 // fixed定位z-index值
},
onScroll: false // 是否监听滚动事件
})
}
/* 配置参数 */
MeScroll.extend = function(userOption, defaultOption) {
if (!userOption) return defaultOption;
for (let key in defaultOption) {
if (userOption[key] == null) {
let def = defaultOption[key];
if (def != null && typeof def === 'object') {
userOption[key] = MeScroll.extend({}, def); // 深度匹配
} else {
userOption[key] = def;
}
} else if (typeof userOption[key] === 'object') {
MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
}
}
return userOption;
}
/* 简单判断是否配置了颜色 (非透明,非白色) */
MeScroll.prototype.hasColor = function(color) {
if (!color) return false;
let c = color.toLowerCase();
return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
}
/* -------初始化下拉刷新------- */
MeScroll.prototype.initDownScroll = function() {
let me = this;
// 配置参数
me.optDown = me.options.down || {};
if (!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor =
"#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
me.extendDownScroll(me.optDown);
// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
if (me.isScrollBody && me.optDown.native) {
me.optDown.use = false
} else {
me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
}
me.downHight = 0; // 下拉区域的高度
// 在页面中加入下拉布局
if (me.optDown.use && me.optDown.inited) {
// 初始化完毕的回调
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
me.optDown.inited(me);
}, 0)
}
}
/* 列表touchstart事件 */
MeScroll.prototype.touchstartEvent = function(e) {
if (!this.optDown.use) return;
this.startPoint = this.getPoint(e); // 记录起点
this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
this.lastPoint = this.startPoint; // 重置上次move的点
this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
this.inTouchend = false; // 标记不是touchend
}
/* 列表touchmove事件 */
MeScroll.prototype.touchmoveEvent = function(e) {
// #ifdef H5
window.isPreventDefault = false // 标记不需要阻止window事件
// #endif
if (!this.optDown.use) return;
if (!this.startPoint) return;
let me = this;
// 节流
let t = new Date().getTime();
if (me.moveTime && t - me.moveTime < me.moveTimeDiff) { // 小于节流时间,则不处理
return;
} else {
me.moveTime = t
if (!me.moveTimeDiff) me.moveTimeDiff = 1000 / me.optDown.fps
}
let scrollTop = me.getScrollTop(); // 当前滚动条的距离
let curPoint = me.getPoint(e); // 当前点
let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
// 向下拉 && 在顶部
// mescroll-body,直接判定在顶部即可
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
// scroll-view滚动到顶部时,scrollTop不一定为0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
if (moveY > 0 && (
(me.isScrollBody && scrollTop <= 0) ||
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me
.startTop)))
)) {
// 可下拉的条件
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me
.isUpScrolling &&
me.optUp.isBoth))) {
// 下拉的角度是否在配置的范围内
let angle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
if (angle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
me.inTouchend = true; // 标记执行touchend
me.touchendEvent(); // 提前触发touchend
return;
}
// #ifdef H5
window.isPreventDefault = true // 标记阻止window事件
// #endif
me.preventDefault(e); // 阻止默认事件
let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
// 下拉距离 < 指定距离
if (me.downHight < me.optDown.offset) {
if (me.movetype !== 1) {
me.movetype = 1; // 加入标记,保证只执行一次
me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
// 指定距离 <= 下拉距离
} else {
if (me.movetype !== 2) {
me.movetype = 2; // 加入标记,保证只执行一次
me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
if (diff > 0) { // 向下拉
me.downHight += Math.round(diff * me.optDown.outOffsetRate); // 越往下,高度变化越小
} else { // 向上收
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
}
}
let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
}
}
me.lastPoint = curPoint; // 记录本次移动的点
}
/* 列表touchend事件 */
MeScroll.prototype.touchendEvent = function(e) {
if (!this.optDown.use) return;
// 如果下拉区域高度已改变,则需重置回来
if (this.isMoveDown) {
if (this.downHight >= this.optDown.offset) {
// 符合触发刷新的条件
this.triggerDownScroll();
} else {
// 不符合的话 则重置
this.downHight = 0;
this.optDown.endDownScroll && this.optDown.endDownScroll(this);
}
this.movetype = 0;
this.isMoveDown = false;
} else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
// 上滑
if (isScrollUp) {
// 需检查滑动的角度
let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
if (angle > 80) {
// 检查并触发上拉
this.triggerUpScroll(true);
}
}
}
}
/* 根据点击滑动事件获取第一个手指的坐标 */
MeScroll.prototype.getPoint = function(e) {
if (!e) {
return {
x: 0,
y: 0
}
}
if (e.touches && e.touches[0]) {
return {
x: e.touches[0].pageX,
y: e.touches[0].pageY
}
} else if (e.changedTouches && e.changedTouches[0]) {
return {
x: e.changedTouches[0].pageX,
y: e.changedTouches[0].pageY
}
} else {
return {
x: e.clientX,
y: e.clientY
}
}
}
/* 计算两点之间的角度: 区间 [0,90]*/
MeScroll.prototype.getAngle = function(p1, p2) {
let x = Math.abs(p1.x - p2.x);
let y = Math.abs(p1.y - p2.y);
let z = Math.sqrt(x * x + y * y);
let angle = 0;
if (z !== 0) {
angle = Math.asin(y / z) / Math.PI * 180;
}
return angle
}
/* 触发下拉刷新 */
MeScroll.prototype.triggerDownScroll = function() {
if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
//return true则处于完全自定义状态
} else {
this.showDownScroll(); // 下拉刷新中...
this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
}
}
/* 显示下拉进度布局 */
MeScroll.prototype.showDownScroll = function() {
this.isDownScrolling = true; // 标记下拉中
if (this.optDown.native) {
uni.startPullDownRefresh(); // 系统自带的下拉刷新
this.optDown.showLoading && this.optDown.showLoading(this, 0); // 仍触发showLoading,因为上拉加载用到
} else {
this.downHight = this.optDown.offset; // 更新下拉区域高度
this.optDown.showLoading && this.optDown.showLoading(this, this.downHight); // 下拉刷新中...
}
}
/* 显示系统自带的下拉刷新时需要处理的业务 */
MeScroll.prototype.onPullDownRefresh = function() {
this.isDownScrolling = true; // 标记下拉中
this.optDown.showLoading && this.optDown.showLoading(this, 0); // 仍触发showLoading,因为上拉加载用到
this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
}
/* 结束下拉刷新 */
MeScroll.prototype.endDownScroll = function() {
if (this.optDown.native) { // 结束原生下拉刷新
this.isDownScrolling = false;
this.optDown.endDownScroll && this.optDown.endDownScroll(this);
uni.stopPullDownRefresh();
return
}
let me = this;
// 结束下拉刷新的方法
let endScroll = function() {
me.downHight = 0;
me.isDownScrolling = false;
me.optDown.endDownScroll && me.optDown.endDownScroll(me);
!me.isScrollBody && me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
}
// 结束下拉刷新时的回调
let delay = 0;
if (me.optDown.afterLoading) delay = me.optDown.afterLoading(me); // 结束下拉刷新的延时,单位ms
if (typeof delay === 'number' && delay > 0) {
setTimeout(endScroll, delay);
} else {
endScroll();
}
}
/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
MeScroll.prototype.lockDownScroll = function(isLock) {
if (isLock == null) isLock = true;
this.optDown.isLock = isLock;
}
/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
MeScroll.prototype.lockUpScroll = function(isLock) {
if (isLock == null) isLock = true;
this.optUp.isLock = isLock;
}
/* -------初始化上拉加载------- */
MeScroll.prototype.initUpScroll = function() {
let me = this;
// 配置参数
me.optUp = me.options.up || {
use: false
}
if (!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor =
"#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
me.extendUpScroll(me.optUp);
if (!me.optUp.isBounce) me.setBounce(false); // 不允许bounce时,需禁止window的touchmove事件
if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
// 初始化完毕的回调
if (me.optUp.inited) {
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
me.optUp.inited(me);
}, 0)
}
}
/*滚动到底部的事件 (仅mescroll-body生效)*/
MeScroll.prototype.onReachBottom = function() {
if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
if (!this.optUp.isLock && this.optUp.hasNext) {
this.triggerUpScroll();
}
}
}
/*列表滚动事件 (仅mescroll-body生效)*/
MeScroll.prototype.onPageScroll = function(e) {
if (!this.isScrollBody) return;
// 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
this.setScrollTop(e.scrollTop);
// 顶部按钮的显示隐藏
if (e.scrollTop >= this.optUp.toTop.offset) {
this.showTopBtn();
} else {
this.hideTopBtn();
}
}
/*列表滚动事件*/
MeScroll.prototype.scroll = function(e, onScroll) {
// 更新滚动条的位置
this.setScrollTop(e.scrollTop);
// 更新滚动内容高度
this.setScrollHeight(e.scrollHeight);
// 向上滑还是向下滑动
if (this.preScrollY == null) this.preScrollY = 0;
this.isScrollUp = e.scrollTop - this.preScrollY > 0;
this.preScrollY = e.scrollTop;
// 上滑 && 检查并触发上拉
this.isScrollUp && this.triggerUpScroll(true);
// 顶部按钮的显示隐藏
if (e.scrollTop >= this.optUp.toTop.offset) {
this.showTopBtn();
} else {
this.hideTopBtn();
}
// 滑动监听
this.optUp.onScroll && onScroll && onScroll()
}
/* 触发上拉加载 */
MeScroll.prototype.triggerUpScroll = function(isCheck) {
if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
// 是否校验在底部; 默认不校验
if (isCheck === true) {
let canUp = false;
// 还有下一页 && 没有锁定 && 不在下拉中
if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
canUp = true; // 标记可上拉
}
}
if (canUp === false) return;
}
this.showUpScroll(); // 上拉加载中...
this.optUp.page.num++; // 预先加一页,如果失败则减回
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
this.optUp.callback(this); // 执行回调,联网加载数据
}
}
/* 显示上拉加载中 */
MeScroll.prototype.showUpScroll = function() {
this.isUpScrolling = true; // 标记上拉加载中
this.optUp.showLoading && this.optUp.showLoading(this); // 回调
}
/* 显示上拉无更多数据 */
MeScroll.prototype.showNoMore = function() {
this.optUp.hasNext = false; // 标记无更多数据
this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
}
/* 隐藏上拉区域**/
MeScroll.prototype.hideUpScroll = function() {
this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
}
/* 结束上拉加载 */
MeScroll.prototype.endUpScroll = function(isShowNoMore) {
if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
if (isShowNoMore) {
this.showNoMore(); // isShowNoMore=true,显示无更多数据
} else {
this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
}
}
this.isUpScrolling = false; // 标记结束上拉加载
}
/* 重置上拉加载列表为第一页
*isShowLoading 是否显示进度布局;
* 1.默认null,不传参,则显示上拉加载的进度布局
* 2.传参true, 则显示下拉刷新的进度布局
* 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
*/
MeScroll.prototype.resetUpScroll = function(isShowLoading) {
if (this.optUp && this.optUp.use) {
let page = this.optUp.page;
this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
page.num = this.startNum; // 重置为第一页
page.time = null; // 重置时间为空
if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
if (isShowLoading == null) {
this.removeEmpty(); // 移除空布局
this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
} else {
this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
}
}
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
}
}
/* 设置page.num的值 */
MeScroll.prototype.setPageNum = function(num) {
this.optUp.page.num = num - 1;
}
/* 设置page.size的值 */
MeScroll.prototype.setPageSize = function(size) {
this.optUp.page.size = size;
}
/* 联网回调成功,结束下拉刷新和上拉加载
* dataSize: 当前页的数据量(必传)
* totalPage: 总页数(必传)
* systime: 服务器时间 (可空)
*/
MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) {
let hasNext;
if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
this.endSuccess(dataSize, hasNext, systime);
}
/* 联网回调成功,结束下拉刷新和上拉加载
* dataSize: 当前页的数据量(必传)
* totalSize: 列表所有数据总数量(必传)
* systime: 服务器时间 (可空)
*/
MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
let hasNext;
if (this.optUp.use && totalSize != null) {
let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
hasNext = loadSize < totalSize; // 是否还有下一页
}
this.endSuccess(dataSize, hasNext, systime);
}
/* 联网回调成功,结束下拉刷新和上拉加载
* dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
* hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
* systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
*/
MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
let me = this;
// 结束下拉刷新
if (me.isDownScrolling) me.endDownScroll();
// 结束上拉加载
if (me.optUp.use) {
let isShowNoMore; // 是否已无更多数据
if (dataSize != null) {
let pageNum = me.optUp.page.num; // 当前页码
let pageSize = me.optUp.page.size; // 每页长度
// 如果是第一页
if (pageNum === 1) {
if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
}
if (dataSize < pageSize || hasNext === false) {
// 返回的数据不满一页时,则说明已无更多数据
me.optUp.hasNext = false;
if (dataSize === 0 && pageNum === 1) {
// 如果第一页无任何数据且配置了空布局
isShowNoMore = false;
me.showEmpty();
} else {
// 总列表数少于配置的数量,则不显示无更多数据
let allDataSize = (pageNum - 1) * pageSize + dataSize;
if (allDataSize < me.optUp.noMoreSize) {
isShowNoMore = false;
} else {
isShowNoMore = true;
}
me.removeEmpty(); // 移除空布局
}
} else {
// 还有下一页
isShowNoMore = false;
me.optUp.hasNext = true;
me.removeEmpty(); // 移除空布局
}
}
// 隐藏上拉
me.endUpScroll(isShowNoMore);
}
}
/* 回调失败,结束下拉刷新和上拉加载 */
MeScroll.prototype.endErr = function(errDistance) {
// 结束下拉,回调失败重置回原来的页码和时间
if (this.isDownScrolling) {
let page = this.optUp.page;
if (page && this.prePageNum) {
page.num = this.prePageNum;
page.time = this.prePageTime;
}
this.endDownScroll();
}
// 结束上拉,回调失败重置回原来的页码
if (this.isUpScrolling) {
this.optUp.page.num--;
this.endUpScroll(false);
// 如果是mescroll-body,则需往回滚一定距离
if (this.isScrollBody && errDistance !== 0) { // 不处理0
if (!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
}
}
}
/* 显示空布局 */
MeScroll.prototype.showEmpty = function() {
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
}
/* 移除空布局 */
MeScroll.prototype.removeEmpty = function() {
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
}
/* 显示回到顶部的按钮 */
MeScroll.prototype.showTopBtn = function() {
if (!this.topBtnShow) {
this.topBtnShow = true;
this.optUp.toTop.onShow && this.optUp.toTop.onShow(true);
}
}
/* 隐藏回到顶部的按钮 */
MeScroll.prototype.hideTopBtn = function() {
if (this.topBtnShow) {
this.topBtnShow = false;
this.optUp.toTop.onShow && this.optUp.toTop.onShow(false);
}
}
/* 获取滚动条的位置 */
MeScroll.prototype.getScrollTop = function() {
return this.scrollTop || 0
}
/* 记录滚动条的位置 */
MeScroll.prototype.setScrollTop = function(y) {
this.scrollTop = y;
}
/* 滚动到指定位置 */
MeScroll.prototype.scrollTo = function(y, t) {
this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
}
/* 自定义scrollTo */
MeScroll.prototype.resetScrollTo = function(myScrollTo) {
this.myScrollTo = myScrollTo
}
/* 滚动条到底部的距离 */
MeScroll.prototype.getScrollBottom = function() {
return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
}
/* 计步器
star: 开始值
end: 结束值
callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
t: 计步时长,传0则直接回调end值;不传则默认300ms
rate: 周期;不传则默认30ms计步一次
* */
MeScroll.prototype.getStep = function(star, end, callback, t, rate) {
let diff = end - star; // 差值
if (t === 0 || diff === 0) {
callback && callback(end);
return;
}
t = t || 300; // 时长 300ms
rate = rate || 30; // 周期 30ms
let count = t / rate; // 次数
let step = diff / count; // 步长
let i = 0; // 计数
let timer = setInterval(function() {
if (i < count - 1) {
star += step;
callback && callback(star, timer);
i++;
} else {
callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
clearInterval(timer);
}
}, rate);
}
/* 滚动容器的高度 */
MeScroll.prototype.getClientHeight = function(isReal) {
let h = this.clientHeight || 0
if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
h = this.getBodyHeight()
}
return h
}
MeScroll.prototype.setClientHeight = function(h) {
this.clientHeight = h;
}
/* 滚动内容的高度 */
MeScroll.prototype.getScrollHeight = function() {
return this.scrollHeight || 0;
}
MeScroll.prototype.setScrollHeight = function(h) {
this.scrollHeight = h;
}
/* body的高度 */
MeScroll.prototype.getBodyHeight = function() {
return this.bodyHeight || 0;
}
MeScroll.prototype.setBodyHeight = function(h) {
this.bodyHeight = h;
}
/* 阻止浏览器默认滚动事件 */
MeScroll.prototype.preventDefault = function(e) {
// 小程序不支持e.preventDefault
// app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止
// cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
}
/* 是否允许下拉回弹(橡皮筋效果); true或null为允许; false禁止bounce */
MeScroll.prototype.setBounce = function(isBounce) {
// #ifdef H5
if (isBounce === false) {
this.optUp.isBounce = false; // 禁止
// 标记当前页使用了mescroll (需延时,确保page已切换)
setTimeout(function() {
let uniPageDom = document.getElementsByTagName('uni-page')[0];
uniPageDom && uniPageDom.setAttribute('use_mescroll', true)
}, 30);
// 避免重复添加事件
if (window.isSetBounce) return;
window.isSetBounce = true;
// 需禁止window的touchmove事件才能有效的阻止bounce
window.bounceTouchmove = function(e) {
if (!window.isPreventDefault) return; // 根据标记判断是否阻止
let el = e.target;
// 当前touch的元素及父元素是否要拦截touchmove事件
let isPrevent = true;
while (el !== document.body && el !== document) {
if (el.tagName === 'UNI-PAGE') { // 只扫描当前页
if (!el.getAttribute('use_mescroll')) {
isPrevent = false; // 如果当前页没有使用mescroll,则不阻止
}
break;
}
let cls = el.classList;
if (cls) {
if (cls.contains('mescroll-touch')) { // 采用scroll-view 此处不能过滤mescroll-uni,否则下拉仍然有回弹
isPrevent = false; // mescroll-touch无需拦截touchmove事件
break;
} else if (cls.contains('mescroll-touch-x') || cls.contains('mescroll-touch-y')) {
// 如果配置了水平或者垂直滑动
let curX = e.touches ? e.touches[0].pageX : e.clientX; // 当前第一个手指距离列表顶部的距离x
let curY = e.touches ? e.touches[0].pageY : e.clientY; // 当前第一个手指距离列表顶部的距离y
if (!this.preWinX) this.preWinX = curX; // 设置上次移动的距离x
if (!this.preWinY) this.preWinY = curY; // 设置上次移动的距离y
// 计算两点之间的角度
let x = Math.abs(this.preWinX - curX);
let y = Math.abs(this.preWinY - curY);
let z = Math.sqrt(x * x + y * y);
this.preWinX = curX; // 记录本次curX的值
this.preWinY = curY; // 记录本次curY的值
if (z !== 0) {
let angle = Math.asin(y / z) / Math.PI * 180; // 角度区间 [0,90]
if ((angle <= 45 && cls.contains('mescroll-touch-x')) || (angle > 45 && cls
.contains('mescroll-touch-y'))) {
isPrevent = false; // 水平滑动或者垂直滑动,不拦截touchmove事件
break;
}
}
}
}
el = el.parentNode; // 继续检查其父元素
}
// 拦截touchmove事件:是否可以被禁用&&是否已经被禁用 (这里不使用me.preventDefault(e)的方法,因为某些情况下会报找不到方法的异常)
if (isPrevent && e.cancelable && !e.defaultPrevented && typeof e.preventDefault === "function") e
.preventDefault();
}
window.addEventListener('touchmove', window.bounceTouchmove, {
passive: false
});
} else {
this.optUp.isBounce = true; // 允许
if (window.bounceTouchmove) {
window.removeEventListener('touchmove', window.bounceTouchmove);
window.bounceTouchmove = null;
window.isSetBounce = false;
}
}
// #endif
}

View File

@@ -0,0 +1,386 @@
<template>
<view class="mescroll-uni-warp">
<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}"
:style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'padding-bottom':padBottomConstant,'padding-bottom':padBottomEnv,'top':fixedTop,'bottom':fixedBottom,'bottom':fixedBottomConstant,'bottom':fixedBottomEnv}"
:scroll-top="scrollTop" :scroll-into-view="scrollToViewId" :scroll-with-animation="scrollAnim"
@scroll="scroll" @touchstart="touchstartEvent" @touchmove="touchmoveEvent" @touchend="touchendEvent"
@touchcancel="touchendEvent" :scroll-y='isDownReset' :enable-back-to-top="true">
<view class="mescroll-uni-content" :style="{'transform': translateY, 'transition': transition}">
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
<view v-if="mescroll.optDown.use" class="mescroll-downwarp"
:style="{'background-color':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
<view class="downwarp-content">
<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}"
:style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
<view class="downwarp-tip">{{downText}}</view>
</view>
</view>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick">
</mescroll-empty>
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading" class="mescroll-upwarp"
:style="{'background-color':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-if="upLoadType===1">
<view class="upwarp-progress mescroll-rotate"
:style="{'border-color':mescroll.optUp.textColor}"></view>
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
</view>
</view>
</scroll-view>
<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
</view>
</template>
<script>
// 引入mescroll-uni.js,处理核心逻辑
import MeScroll from './mescroll-uni.js';
// 引入全局配置
import GlobalOption from './mescroll-uni-option.js';
// 引入空布局组件
import MescrollEmpty from './components/mescroll-empty.vue';
// 引入回到顶部组件
import MescrollTop from './components/mescroll-top.vue';
export default {
components: {
MescrollEmpty,
MescrollTop
},
data() {
return {
mescroll: {
optDown: {},
optUp: {}
}, // mescroll实例
viewId: 'id_' + Math.random().toString(36).substr(2), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
downHight: 0, //下拉刷新: 容器高度
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
downLoadType: 4, // 下拉刷新状态 inOffset1 outOffset2 showLoading3 endDownScroll4
upLoadType: 0, // 上拉加载状态0loading前1loading中2没有更多了
isShowEmpty: false, // 是否显示空布局
isShowToTop: false, // 是否显示回到顶部按钮
scrollTop: 0, // 滚动条的位置
scrollAnim: false, // 是否开启滚动动画
windowTop: 0, // 可使用窗口的顶部位置
windowBottom: 0, // 可使用窗口的底部位置
windowHeight: 0, // 可使用窗口的高度
statusBarHeight: 0, // 状态栏高度
isSafearea: false, // 支持安全区
scrollToViewId: '' // 滚动到指定view的id
}
},
props: {
down: Object, // 下拉刷新的参数配置
up: Object, // 上拉加载的参数配置
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
topbar: Boolean, // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可自动加上状态栏高度的偏移量)
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
fixed: { // 是否通过fixed固定mescroll的高度, 默认true
type: Boolean,
default () {
return true
}
},
height: [String,
Number
] // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
},
computed: {
// 是否使用fixed定位 (当height有值,则不使用)
isFixed() {
return !this.height && this.fixed
},
// mescroll的高度
scrollHeight() {
if (this.isFixed) {
return "auto"
} else if (this.height) {
return this.toPx(this.height) + 'px'
} else {
return "100%"
}
},
// 下拉布局往下偏移的距离 (px)
numTop() {
return this.toPx(this.top) + (this.topbar ? this.statusBarHeight : 0)
},
fixedTop() {
return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
},
padTop() {
return !this.isFixed ? this.numTop + 'px' : 0
},
// 上拉布局往上偏移 (px)
numBottom() {
return this.toPx(this.bottom)
},
fixedBottom() {
return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
},
fixedBottomConstant() {
return this.isSafearea ? "calc(" + this.fixedBottom + " + constant(safe-area-inset-bottom))" : this
.fixedBottom
},
fixedBottomEnv() {
return this.isSafearea ? "calc(" + this.fixedBottom + " + env(safe-area-inset-bottom))" : this.fixedBottom
},
padBottom() {
return !this.isFixed ? this.numBottom + 'px' : 0
},
padBottomConstant() {
return this.isSafearea ? "calc(" + this.padBottom + " + constant(safe-area-inset-bottom))" : this.padBottom
},
padBottomEnv() {
return this.isSafearea ? "calc(" + this.padBottom + " + env(safe-area-inset-bottom))" : this.padBottom
},
// 是否为重置下拉的状态
isDownReset() {
return this.downLoadType === 3 || this.downLoadType === 4
},
// 过渡
transition() {
return this.isDownReset ? 'transform 300ms' : '';
},
translateY() {
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' :
''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
},
// 是否在加载中
isDownLoading() {
return this.downLoadType === 3
},
// 旋转的角度
downRotate() {
return 'rotate(' + 360 * this.downRate + 'deg)'
},
// 文本提示
downText() {
switch (this.downLoadType) {
case 1:
return this.mescroll.optDown.textInOffset;
case 2:
return this.mescroll.optDown.textOutOffset;
case 3:
return this.mescroll.optDown.textLoading;
case 4:
return this.mescroll.optDown.textLoading;
default:
return this.mescroll.optDown.textInOffset;
}
}
},
methods: {
//number,rpx,upx,px,% --> px的数值
toPx(num) {
if (typeof num === "string") {
if (num.indexOf('px') !== -1) {
if (num.indexOf('rpx') !== -1) { // "10rpx"
num = num.replace('rpx', '');
} else if (num.indexOf('upx') !== -1) { // "10upx"
num = num.replace('upx', '');
} else { // "10px"
return Number(num.replace('px', ''))
}
} else if (num.indexOf('%') !== -1) {
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
let rate = Number(num.replace("%", "")) / 100
return this.windowHeight * rate
}
}
return num ? uni.upx2px(Number(num)) : 0
},
//注册列表滚动事件,用于下拉刷新和上拉加载
scroll(e) {
this.mescroll.scroll(e.detail, () => {
this.$emit('scroll', this
.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
})
},
//注册列表touchstart事件,用于下拉刷新
touchstartEvent(e) {
this.mescroll.touchstartEvent(e);
},
//注册列表touchmove事件,用于下拉刷新
touchmoveEvent(e) {
this.mescroll.touchmoveEvent(e);
},
//注册列表touchend事件,用于下拉刷新
touchendEvent(e) {
this.mescroll.touchendEvent(e);
},
// 点击空布局的按钮回调
emptyClick() {
this.$emit('emptyclick', this.mescroll)
},
// 点击回到顶部的按钮回调
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
},
// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
setClientHeight() {
if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
this.isExec = true; // 避免多次获取
this.$nextTick(() => { // 确保dom已渲染
let view = uni.createSelectorQuery().in(this).select('#' + this.viewId);
view.boundingClientRect(data => {
this.isExec = false;
if (data) {
this.mescroll.setClientHeight(data.height);
} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
setTimeout(() => {
this.setClientHeight()
}, this.clientNum * 100)
}
}).exec();
})
}
}
},
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
created() {
let vm = this;
let diyOption = {
// 下拉刷新的配置
down: {
inOffset(mescroll) {
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
},
outOffset(mescroll) {
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
},
onMoving(mescroll, rate, downHight) {
// 下拉过程中的回调,滑动过程一直在执行;
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
},
showLoading(mescroll, downHight) {
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
endDownScroll(mescroll) {
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
// 派发下拉刷新的回调
callback: function(mescroll) {
vm.$emit('down', mescroll)
}
},
// 上拉加载的配置
up: {
// 显示加载中的回调
showLoading() {
vm.upLoadType = 1;
},
// 显示无更多数据的回调
showNoMore() {
vm.upLoadType = 2;
},
// 隐藏上拉加载的回调
hideUpScroll() {
vm.upLoadType = 0;
},
// 空布局
empty: {
onShow(isShow) { // 显示隐藏的回调
vm.isShowEmpty = isShow;
}
},
// 回到顶部
toTop: {
onShow(isShow) { // 显示隐藏的回调
vm.isShowToTop = isShow;
}
},
// 派发上拉加载的回调
callback: function(mescroll) {
vm.$emit('up', mescroll);
// 更新容器的高度 (多mescroll的情况)
vm.setClientHeight()
}
}
}
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
let myOption = JSON.parse(JSON.stringify({
'down': vm.down,
'up': vm.up
})) // 深拷贝,避免对props的影响
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
// 初始化MeScroll对象
vm.mescroll = new MeScroll(myOption);
vm.mescroll.viewId = vm.viewId; // 附带id
// init回调mescroll对象
vm.$emit('init', vm.mescroll);
// 设置高度
const sys = uni.getSystemInfoSync();
if (sys.windowTop) vm.windowTop = sys.windowTop;
if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
// 使down的bottomOffset生效
vm.mescroll.setBodyHeight(sys.windowHeight);
// 因为使用的是scrollview,这里需自定义scrollTo
vm.mescroll.resetScrollTo((y, t) => {
vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
if (typeof y === 'string') { // 第一个参数如果为字符串,则使用scroll-into-view
vm.scrollToViewId = y;
return;
}
let curY = vm.mescroll.getScrollTop()
if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
vm.scrollTop = curY;
vm.$nextTick(function() {
vm.scrollTop = y
})
} else {
vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
vm.scrollTop = step
}, t)
}
})
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
if (sys.platform == "ios") {
vm.isSafearea = vm.safearea;
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
} else {
vm.isSafearea = false
vm.mescroll.optUp.toTop.safearea = false
}
},
mounted() {
// 设置容器的高度
this.setClientHeight()
}
}
</script>
<style>
@import "./mescroll-uni.css";
@import "./components/mescroll-down.css";
@import './components/mescroll-up.css';
</style>

View File

@@ -0,0 +1,23 @@
/**
* mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期:
* 当一个页面只有一个mescroll-body写在子组件时, 则使用mescroll-comp.js (参考 mescroll-comp 案例)
* 当一个页面有多个mescroll-body写在子组件时, 则使用mescroll-more.js (参考 mescroll-more 案例)
*/
const MescrollCompMixin = {
// 因为子组件无onPageScroll和onReachBottom的页面生命周期需在页面传递进到子组件
onPageScroll(e) {
let item = this.$refs["mescrollItem"];
if (item && item.mescroll) item.mescroll.onPageScroll(e);
},
onReachBottom() {
let item = this.$refs["mescrollItem"];
if (item && item.mescroll) item.mescroll.onReachBottom();
},
// 当down的native: true时, 还需传递此方法进到子组件
onPullDownRefresh() {
let item = this.$refs["mescrollItem"];
if (item && item.mescroll) item.mescroll.onPullDownRefresh();
}
}
export default MescrollCompMixin;

View File

@@ -0,0 +1,48 @@
/**
* mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
*/
const MescrollMoreItemMixin = {
props: {
i: Number, // 每个tab页的专属下标
index: { // 当前tab的下标
type: Number,
default () {
return 0
}
}
},
data() {
return {
downOption: {
auto: false // 不自动加载
},
upOption: {
auto: false // 不自动加载
},
isInit: false // 当前tab是否已初始化
}
},
watch: {
// 监听下标的变化
index(val) {
if (this.i === val && !this.isInit) {
this.isInit = true; // 标记为true
this.mescroll && this.mescroll.triggerDownScroll();
}
}
},
methods: {
// mescroll组件初始化的回调,可获取到mescroll对象
mescrollInit(mescroll) {
this.mescroll = mescroll;
this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序 (mescroll-mixins.js)
// 自动加载当前tab的数据
if (this.i === this.index) {
this.isInit = true; // 标记为true
this.mescroll.triggerDownScroll();
}
},
}
}
export default MescrollMoreItemMixin;

View File

@@ -0,0 +1,56 @@
/**
* mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期:
* 当一个页面只有一个mescroll-body写在子组件时, 则使用mescroll-comp.js (参考 mescroll-comp 案例)
* 当一个页面有多个mescroll-body写在子组件时, 则使用mescroll-more.js (参考 mescroll-more 案例)
*/
const MescrollMoreMixin = {
data() {
return {
tabIndex: 0 // 当前tab下标
}
},
// 因为子组件无onPageScroll和onReachBottom的页面生命周期需在页面传递进到子组件
onPageScroll(e) {
let mescroll = this.getMescroll(this.tabIndex);
mescroll && mescroll.onPageScroll(e);
},
onReachBottom() {
let mescroll = this.getMescroll(this.tabIndex);
mescroll && mescroll.onReachBottom();
},
// 当down的native: true时, 还需传递此方法进到子组件
onPullDownRefresh() {
let mescroll = this.getMescroll(this.tabIndex);
mescroll && mescroll.onPullDownRefresh();
},
methods: {
// 根据下标获取对应子组件的mescroll
getMescroll(i) {
if (!this.mescrollItems) this.mescrollItems = [];
if (!this.mescrollItems[i]) {
// v-for中的refs
let vForItem = this.$refs["mescrollItem"];
if (vForItem) {
this.mescrollItems[i] = vForItem[i]
} else {
// 普通的refs,不可重复
this.mescrollItems[i] = this.$refs["mescrollItem" + i];
}
}
let item = this.mescrollItems[i]
return item ? item.mescroll : null
},
// 切换tab,恢复滚动条位置
tabChange(i) {
let mescroll = this.getMescroll(i);
if (mescroll) {
// 延时(比$nextTick靠谱一些),确保元素已渲染
setTimeout(() => {
mescroll.scrollTo(mescroll.getScrollTop(), 0)
}, 30)
}
}
}
}
export default MescrollMoreMixin;

View File

@@ -0,0 +1,20 @@
<template>
<view style="display: flex;justify-content: center;width: 100%;">
<image style="width: 280rpx;height: 280rpx; margin-top: 130rpx;margin-bottom: 110rpx;"
src="../../static/custom-icon/zanwushuju.png" mode="aspectFill"></image>
</view>
</template>
<script>
export default {
data() {
return {
};
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,135 @@
<template>
<picker mode="multiSelector" :value="multiIndex" :range="multiArray" @change="handleValueChange"
@columnchange="handleColumnChange">
<slot></slot>
</picker>
</template>
<script>
const CHINA_REGIONS = require('./regions.json')
export default {
props: {
defaultRegions: {
type: Array,
default () {
return []
}
},
defaultRegionCode: {
type: String
},
defaultRegion: [String, Array]
},
data() {
return {
cityArr: CHINA_REGIONS[0].childs,
districtArr: CHINA_REGIONS[0].childs[0].childs,
multiIndex: [0, 0, 0],
isInitMultiArray: true,
}
},
watch: {
defaultRegion: {
handler(region, oldRegion) {
if (Array.isArray(region)) {
// 避免传的是字面量的时候重复触发
oldRegion = oldRegion || []
if (region.join('') !== oldRegion.join('')) {
this.handleDefaultRegion(region)
}
} else if (region && region.length == 6) {
this.handleDefaultRegion(region)
} else {
console.warn('defaultRegion非有效格式')
}
},
immediate: true,
}
},
computed: {
multiArray() {
return this.pickedArr.map(arr => arr.map(item => item.name))
},
pickedArr() {
// 进行初始化
if (this.isInitMultiArray) {
return [
CHINA_REGIONS,
CHINA_REGIONS[0].childs,
CHINA_REGIONS[0].childs[0].childs
]
}
return [CHINA_REGIONS, this.cityArr, this.districtArr];
}
},
methods: {
handleColumnChange(e) {
// console.log(e);
this.isInitMultiArray = false;
const that = this;
let col = e.detail.column;
let row = e.detail.value;
that.multiIndex[col] = row;
try {
switch (col) {
case 0:
if (CHINA_REGIONS[that.multiIndex[0]].childs.length == 0) {
that.cityArr = that.districtArr = [CHINA_REGIONS[that.multiIndex[0]]]
break;
}
that.cityArr = CHINA_REGIONS[that.multiIndex[0]].childs
that.districtArr = CHINA_REGIONS[that.multiIndex[0]].childs[that.multiIndex[1]].childs
break;
case 1:
that.districtArr = CHINA_REGIONS[that.multiIndex[0]].childs[that.multiIndex[1]].childs
break;
case 2:
break;
}
} catch (e) {
// console.log(e);
that.districtArr = CHINA_REGIONS[that.multiIndex[0]].childs[0].childs
}
},
handleValueChange(e) {
// 结构赋值
let [index0, index1, index2] = e.detail.value;
let [arr0, arr1, arr2] = this.pickedArr;
let address = [arr0[index0], arr1[index1], arr2[index2]];
// console.log(address);
this.$emit('getRegion', address)
},
handleDefaultRegion(region) {
const isCode = !Array.isArray(region)
this.isInitMultiArray = false;
let children = CHINA_REGIONS
for (let i = 0; i < 3; i++) {
for (let j = 0; j < children.length; j++) {
let condition = isCode ? children[j].code == region.slice(0, (i + 1) * 2) : children[j].name
.includes(region[i]);
if (condition) {
// 匹配成功进行赋值
// console.log(i,j,children.length-1);
children = children[j].childs;
if (i == 0) {
this.cityArr = children
} else if (i == 1) {
this.districtArr = children
}
this.$set(this.multiIndex, i, j)
// console.log(this.multiIndex);
break;
} else {
// 首次匹配失败就用默认的初始化
// console.log(i,j,children.length-1);
if (i == 0 && j == (children.length - 1)) {
this.isInitMultiArray = true;
}
}
}
}
}
},
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,459 @@
<template>
<view>
<view style="padding: 0px 0px;">
<view class="filter-content" v-for="(item, index) in menuList" :key="index" v-if="menuIndex == index">
<view class="filter-content-list">
<view v-for="(detailItem,idx) in selectDetailList" :key="idx"
:class="detailItem.isSelected?'filter-content-list-item-active':'filter-content-list-item-default'"
:style="{'color': detailItem.isSelected?themeColor:'#666666'}"
@tap="sortTap(idx,selectDetailList,item.key)">
<text>{{detailItem.title}}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
selectArr: [],
result: {},
menuIndex: 0,
selectDetailList: [],
independenceObj: {},
selectedKey: '',
cacheSelectedObj: {},
defaultSelectedTitleObj: {}
};
},
props: {
themeColor: {
type: String,
default () {
return '#D1372C'
}
},
menuList: {
type: Array,
default () {
return []
}
},
independence: {
type: Boolean,
default: true
}
},
computed: {
selectedTitleObj() {
let obj = {}
for (let i = 0; i < this.menuList.length; i++) {
let item = this.menuList[i];
obj[item.key] = item.title;
}
return obj;
},
defaultSelectedObj() { // 保存初始状态
return this.getSelectedObj()
},
selectedObj: {
get() {
return this.getSelectedObj()
},
set(newObj) {
return newObj;
}
}
},
methods: {
getSelectedObj() {
let obj = {}
for (let i = 0; i < this.menuList.length; i++) {
let item = this.menuList[i];
if (!this.independence && item.defaultSelectedIndex != null && item.defaultSelectedIndex.toString()
.length > 0) { // 处理并列菜单默认值
if (item.isMutiple) {
obj[item.key] = [];
item.detailList[0].isSelected = false;
if (!Array.isArray(item.defaultSelectedIndex)) { // 如果默认值不是数组
item.defaultSelectedIndex = [item.defaultSelectedIndex];
}
for (let j = 0; j < item.defaultSelectedIndex.length; j++) { // 将默认选中的值放入selectedObj
item.detailList[item.defaultSelectedIndex[j]].isSelected = true;
obj[item.key].push(item.detailList[item.defaultSelectedIndex[j]].value)
}
} else {
obj[item.key] = item.detailList[item.defaultSelectedIndex].value;
this.selectedTitleObj[item.key] = item.detailList[item.defaultSelectedIndex].title;
this.defaultSelectedTitleObj[item.key] = item.detailList[item.defaultSelectedIndex].title;
item.detailList[0].isSelected = false;
item.detailList[item.defaultSelectedIndex].isSelected = true;
}
} else {
if (item.isMutiple) {
obj[item.key] = [];
} else {
obj[item.key] = '';
}
}
}
this.result = obj;
return obj;
},
// 重置所有选项包括默认选项并更新result
resetAllSelect(callback) {
let titles = [];
for (let i = 0; i < this.menuList.length; i++) {
this.resetSelected(this.menuList[i].detailList, this.menuList[i].key);
titles[this.menuList[i].key] = this.menuList[i].title;
}
let obj = {
'result': this.result,
'titles': titles,
'isReset': true
}
this.$emit("confirm", obj);
callback(this.result);
},
// 重置选项为设置的默认值并更新result
resetSelectToDefault(callback) {
for (let i = 0; i < this.menuList.length; i++) {
this.selectDetailList = this.menuList[i].detailList;
if (this.menuList[i].defaultSelectedIndex) {
if (Array.isArray(this.menuList[i].defaultSelectedIndex)) { // 把所有默认的为false的点为true
for (let j = 0; j < this.menuList[i].defaultSelectedIndex.length; j++) {
if (this.selectDetailList[this.menuList[i].defaultSelectedIndex[j]].isSelected == false) {
this.itemTap(this.menuList[i].defaultSelectedIndex[j], this.selectDetailList, this
.menuList[i].isMutiple, this
.menuList[i].key)
}
}
} else {
this.itemTap(this.menuList[i].defaultSelectedIndex, this.selectDetailList, this.menuList[i]
.isMutiple, this.menuList[
i].key)
}
// 获取非默认项的下标
let unDefaultSelectedIndexArr = this.getUnDefaultSelectedIndex(this.menuList[i])
// 把所有不是默认的为true的点为false
for (let j = 0; j < unDefaultSelectedIndexArr.length; j++) {
if (this.selectDetailList[unDefaultSelectedIndexArr[j]].isSelected == true) {
this.itemTap(unDefaultSelectedIndexArr[j], this.selectDetailList, this.menuList[i]
.isMutiple, this
.menuList[i].key)
}
}
}
}
this.selectedObj = this.defaultSelectedObj;
this.result = this.defaultSelectedObj;
let obj = {
'result': this.result,
'titles': this.defaultSelectedTitleObj,
'isReset': true
}
this.$emit("confirm", obj);
callback(this.result)
},
getUnDefaultSelectedIndex(menuListItem) { // 获取非默认项
let tempDefault = menuListItem.defaultSelectedIndex;
if (!Array.isArray(tempDefault)) {
tempDefault = [tempDefault];
}
// 获取所有项的下标 组成新的数组
let all = [];
for (let i = 0; i < menuListItem.detailList.length; i++) {
all.push(i)
}
// 将默认选中的数组与所有项的数组的不同值合并为一个新数组
var unDefaultSelectedIndex = tempDefault.filter(function(v) {
return !(all.indexOf(v) > -1)
}).concat(all.filter(function(v) {
return !(tempDefault.indexOf(v) > -1)
}));
return unDefaultSelectedIndex;
},
resetMenuList(val) {
this.menuList = val;
this.$emit('update:menuList', val)
},
menuTabClick(index) {
this.menuIndex = index;
this.selectDetailList = this.menuList[index].detailList;
this.selectedKey = this.menuList[index].key;
// 如果是独立菜单
if (this.independence && !this.menuList[index].isSort) {
if (JSON.stringify(this.independenceObj) == '{}') {
this.initIndependenceObj(index);
} else {
for (let key in this.independenceObj) {
if (key != this.selectedKey) {
this.initIndependenceObj(index);
this.resetSelected(this.menuList[index].detailList, this.selectedKey);
}
}
}
}
if (this.independence && this.menuList[index].isSort) {
this.independenceObj = {};
}
if (this.independence) {
let idx = this.menuList[index].defaultSelectedIndex;
if (idx != null && idx.toString().length > 0) { // 处理独立菜单默认值
if (this.menuList[index].isMutiple) {
for (let i = 0; i < idx.length; i++) {
if (this.menuList[index].detailList[idx[i]].isSelected == false) {
this.itemTap(idx[i], this.menuList[index].detailList, true, this.selectedKey);
}
}
} else {
if (this.menuList[index].detailList[idx].isSelected == false) {
this.itemTap(idx, this.menuList[index].detailList, false, this.selectedKey);
}
}
}
}
this.selectedObj = this.selectedObj;
this.$forceUpdate();
},
initIndependenceObj(index) {
this.independenceObj = {};
if (this.menuList[index].isMutiple) {
this.independenceObj[this.selectedKey] = [];
} else {
this.independenceObj[this.selectedKey] = '';
}
},
itemTap(index, list, isMutiple, key) {
if (isMutiple == true) {
list[index].isSelected = !list[index].isSelected;
if (index == 0) {
this.resetSelected(list, key)
if (!this.independence) {
this.selectedTitleObj[key] = list[index].title;
}
} else {
list[0].isSelected = false
if (list[index].isSelected) {
if (this.independence) {
this.independenceObj[this.selectedKey].push(list[index].value);
} else {
this.selectedObj[key].push(list[index].value);
}
} else {
list[index].isSelected = false;
if (this.independence) {
var idx = this.independenceObj[this.selectedKey].indexOf(list[index].value);
this.independenceObj[this.selectedKey].splice(idx, 1);
} else {
var idx = this.selectedObj[key].indexOf(list[index].value);
this.selectedObj[key].splice(idx, 1);
}
}
if (this.independence) {
this.result = this.independenceObj;
} else {
this.result = this.selectedObj;
}
}
} else {
if (index == 0) {
this.resetSelected(list, key)
if (!this.independence) {
this.selectedTitleObj[key] = list[index].title;
}
} else {
list[0].isSelected = false
if (this.independence) {
this.independenceObj[this.selectedKey] = list[index].value;
this.result = this.independenceObj;
} else {
this.selectedObj[key] = list[index].value;
this.result = this.selectedObj;
this.selectedTitleObj[key] = list[index].title;
}
for (let i = 0; i < list.length; i++) {
if (index == i) {
list[i].isSelected = true
} else {
list[i].isSelected = false
}
}
}
}
// #ifdef H5
this.$forceUpdate();
// #endif
},
resetSelected(list, key) {
if (typeof this.result[key] == 'object') {
this.result[key] = [];
this.selectedTitleObj[key] = list[0].title;
} else {
this.result[key] = '';
this.selectedTitleObj[key] = list[0].title;
}
for (let i = 0; i < list.length; i++) {
if (i == 0) {
list[i].isSelected = true;
} else {
list[i].isSelected = false;
}
}
// #ifdef H5
this.$forceUpdate();
// #endif
},
sortTap(index, list, key) {
if (this.independence) {
this.independenceObj[this.selectedKey] = list[index].value;
this.result = this.independenceObj;
} else {
this.selectedObj[key] = list[index].value;
this.result = this.selectedObj;
this.selectedTitleObj[key] = list[index].title;
}
for (let i = 0; i < list.length; i++) {
if (index == i) {
list[i].isSelected = true;
} else {
list[i].isSelected = false;
}
}
let obj = {
'result': this.result,
'titles': this.selectedTitleObj,
'isReset': false
}
this.$emit("confirm", obj);
},
sureClick() {
let obj = {
'result': this.result,
'titles': this.selectedTitleObj,
'isReset': false
}
this.$emit("confirm", obj);
},
resetClick(list, key) {
this.resetSelected(list, key)
}
}
}
</script>
<style>
.filter-content {
background-color: #F6F7F8;
}
.filter-content-title {
border-bottom: #EEEEEE 1px solid;
padding: 10px 15px;
font-size: 13px;
color: #999999;
}
.filter-content-detail {
padding: 5px 15px;
}
.filter-content-detail-item-active {
background-color: #D1372C;
color: #FFFFFF;
padding: 5px 15px;
border-radius: 20px;
margin-right: 10px;
margin-top: 10px;
display: inline-block;
font-size: 14px;
}
.filter-content-detail-item-default {
background-color: #FFFFFF;
color: #666666;
padding: 5px 15px;
border-radius: 20px;
margin-right: 10px;
margin-top: 10px;
display: inline-block;
font-size: 14px;
}
.filter-content-footer {
display: flex;
justify-content: space-between;
width: 100%;
height: 45px;
margin-top: 10px;
}
.filter-content-footer-item {
width: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
}
.filter-content-list {
padding: 5px 15px;
}
.filter-content-list-item-default {
color: #666666;
width: 100%;
padding: 10px 0px;
}
.filter-content-list-item-default text {
width: 90%;
font-size: 14px;
display: inline-block;
}
.filter-content-list-item-active {
color: #D1372C;
width: 100%;
padding: 10px 0px;
}
.filter-content-list-item-active text {
font-size: 14px;
width: 90%;
display: inline-block;
}
.filter-content-list-item-active:after {
content: '✓';
}
</style>

View File

@@ -0,0 +1,20 @@
@font-face {
font-family: 'sl-font';
src: url('data:font/truetype;charset=utf-8;base64,AAEAAAALAIAAAwAwR1NVQrD+s+0AAAE4AAAAQk9TLzI8kEgOAAABfAAAAFZjbWFwZO3RAgAAAeAAAAGGZ2x5Zh0ZI/EAAANwAAAAyGhlYWQVZkUXAAAA4AAAADZoaGVhB94DhAAAALwAAAAkaG10eAwAAAAAAAHUAAAADGxvY2EAMgBkAAADaAAAAAhtYXhwAREAKAAAARgAAAAgbmFtZT5U/n0AAAQ4AAACbXBvc3TohGjqAAAGqAAAADMAAQAAA4D/gABcBAAAAAAABAAAAQAAAAAAAAAAAAAAAAAAAAMAAQAAAAEAANxW6kVfDzz1AAsEAAAAAADZJADbAAAAANkkANsAAAAABAACZAAAAAgAAgAAAAAAAAABAAAAAwAcAAQAAAAAAAIAAAAKAAoAAAD/AAAAAAAAAAEAAAAKAB4ALAABREZMVAAIAAQAAAAAAAAAAQAAAAFsaWdhAAgAAAABAAAAAQAEAAQAAAABAAgAAQAGAAAAAQAAAAAAAQQAAZAABQAIAokCzAAAAI8CiQLMAAAB6wAyAQgAAAIABQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUGZFZABA5hrmHAOA/4AAXAOAAIAAAAABAAAAAAAABAAAAAQAAAAEAAAAAAAABQAAAAMAAAAsAAAABAAAAV4AAQAAAAAAWAADAAEAAAAsAAMACgAAAV4ABAAsAAAABgAEAAEAAuYa5hz//wAA5hrmHP//AAAAAAABAAYABgAAAAEAAgAAAQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAKAAAAAAAAAACAADmGgAA5hoAAAABAADmHAAA5hwAAAACAAAAAAAAADIAZAAEAAAAAAOlAmQAEwAWABkAGgAAEwEWMjcBNjIWFAcBBiInASY0NjIBMDEVMDEnmQFgAgoDAV8LHRUK/n8LHAv+fwoVHQFoAQJZ/qEDAwFfCxYcC/6ACwsBgAsdFf6bAgQAAAAABAAAAAADpAJkABMAFgAZABsAACUBJiIHAQYiJjQ3ATYyFwEWFAYiATAxNTAxFzEDZ/6hAwoD/qELHRUKAYELHAsBgQoVHf6YAacBXwMD/qELFhwLAYEKCv5/CxwWAWUCBAAAAAAAEgDeAAEAAAAAAAAAFQAAAAEAAAAAAAEACAAVAAEAAAAAAAIABwAdAAEAAAAAAAMACAAkAAEAAAAAAAQACAAsAAEAAAAAAAUACwA0AAEAAAAAAAYACAA/AAEAAAAAAAoAKwBHAAEAAAAAAAsAEwByAAMAAQQJAAAAKgCFAAMAAQQJAAEAEACvAAMAAQQJAAIADgC/AAMAAQQJAAMAEADNAAMAAQQJAAQAEADdAAMAAQQJAAUAFgDtAAMAAQQJAAYAEAEDAAMAAQQJAAoAVgETAAMAAQQJAAsAJgFpCkNyZWF0ZWQgYnkgaWNvbmZvbnQKaWNvbmZvbnRSZWd1bGFyaWNvbmZvbnRpY29uZm9udFZlcnNpb24gMS4waWNvbmZvbnRHZW5lcmF0ZWQgYnkgc3ZnMnR0ZiBmcm9tIEZvbnRlbGxvIHByb2plY3QuaHR0cDovL2ZvbnRlbGxvLmNvbQAKAEMAcgBlAGEAdABlAGQAIABiAHkAIABpAGMAbwBuAGYAbwBuAHQACgBpAGMAbwBuAGYAbwBuAHQAUgBlAGcAdQBsAGEAcgBpAGMAbwBuAGYAbwBuAHQAaQBjAG8AbgBmAG8AbgB0AFYAZQByAHMAaQBvAG4AIAAxAC4AMABpAGMAbwBuAGYAbwBuAHQARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABzAHYAZwAyAHQAdABmACAAZgByAG8AbQAgAEYAbwBuAHQAZQBsAGwAbwAgAHAAcgBvAGoAZQBjAHQALgBoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAAAAAAIAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwECAQMBBAAEZG93bgJ1cAAAAA==') format('truetype');
}
.sl-font {
font-family: "sl-font" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.sl-down:before {
content: "\e61a";
}
.sl-up:before {
content: "\e61c";
}

View File

@@ -0,0 +1,122 @@
<template>
<scroll-view scroll-y v-show="ifshow" @tap="ableClose" @touchmove.stop.prevent class="popup-layer">
<view ref="popRef" class="popup-content" @tap.stop="stopEvent" :style="_location">
<slot></slot>
</view>
</scroll-view>
</template>
<script>
export default {
name: 'popup-layer',
props: {
direction: {
type: String,
default: 'top', // 方向 topbottomleftright
},
autoClose: {
type: Boolean,
default: true,
},
isTransNav: {
type: Boolean,
default: false
},
navHeight: {
type: Number,
default: 0
}
},
data() {
return {
ifshow: false, // 是否展示,
translateValue: -100, // 位移距离
timer: null,
iftoggle: false,
};
},
computed: {
_translate() {
if (this.isTransNav) {
const transformObj = {
'top': `transform:translateY(${-this.translateValue}%)`,
'bottom': `transform:translateY(calc(${this.translateValue}% + ${this.navHeight}px))`,
'left': `transform:translateX(${-this.translateValue}%)`,
'right': `transform:translateX(${this.translateValue}%)`
};
return transformObj[this.direction]
} else {
const transformObj = {
'top': `transform:translateY(${-this.translateValue}%)`,
'bottom': `transform:translateY(${this.translateValue}%)`,
'left': `transform:translateX(${-this.translateValue}%)`,
'right': `transform:translateX(${this.translateValue}%)`
};
return transformObj[this.direction]
}
},
_location() {
const positionValue = {
'top': 'bottom:0px;width:100%;',
'bottom': 'top:0px;width:100%;',
'left': 'right:0px;height:100%;',
'right': 'left:0px;height:100%;',
};
return positionValue[this.direction] + this._translate;
}
},
methods: {
show() {
let _this = this;
this.ifshow = true;
let _open = setTimeout(() => {
this.translateValue = 0;
_open = null;
}, 100)
let _toggle = setTimeout(() => {
this.iftoggle = true;
_toggle = null;
}, 300);
},
close() {
if (this.timer !== null || !this.iftoggle) {
return;
}
this.translateValue = -100 - this.navHeight;
this.timer = setTimeout(() => {
this.ifshow = false;
this.timer = null;
this.iftoggle = false;
}, 300);
this.$emit("close")
},
ableClose() {
if (this.autoClose) {
this.close();
}
},
stopEvent(event) {},
}
}
</script>
<style>
.popup-layer {
position: absolute;
z-index: 999999;
background: rgba(0, 0, 0, .3);
height: calc(100% - 50px);
width: 100%;
left: 0px;
overflow: hidden;
}
.popup-content {
position: absolute;
z-index: 1000000;
background: #FFFFFF;
transition: all .3s ease;
}
</style>

View File

@@ -0,0 +1,301 @@
<template>
<view class="content">
<view :style="{height: tabHeight + 1 +'px'}">
<view :class="topFixed?'select-tab-fixed-top':'select-tab'" :style="{height: tabHeight+'px'}">
<view class="select-tab-item" :style="{width: itemWidth}" v-for="(item,index) in titleList" :key="index"
@tap="showMenuClick(index)">
<text style="display:-webkit-box;-webkit-line-clamp:1;
overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;
word-break:break-all; " :style="{color:color}">{{item.title}}</text>
<text class="arrows sl-font" :class="statusList[index].isActive?up:down"></text>
</view>
</view>
</view>
<popup-layer ref="popupRef" :direction="'bottom'" @close="close" :isTransNav="isTransNav" :navHeight="navHeight"
:tabHeight="tabHeight">
<sl-filter-view :ref="'slFilterView'" :independence="independence" :themeColor="themeColor"
:menuList.sync="menuListTemp" ref="slFilterView" @confirm="filterResult"></sl-filter-view>
</popup-layer>
</view>
</template>
<script>
import popupLayer from '@/components/sl-filter/popup-layer.vue';
import slFilterView from '@/components/sl-filter/filter-view.vue';
export default {
components: {
popupLayer,
slFilterView
},
props: {
menuList: {
type: Array,
default () {
return []
}
},
themeColor: {
type: String,
default () {
return '#000000'
}
},
color: {
type: String,
default () {
return '#666666'
}
},
independence: {
type: Boolean,
default: true
},
isTransNav: {
type: Boolean,
default: false
},
navHeight: {
type: Number,
default: 0
},
topFixed: {
type: Boolean,
default: false
}
},
computed: {
itemWidth() {
return 'calc(100%/2)'
},
menuListTemp: {
get() {
return this.getMenuListTemp();
},
set(newObj) {
return newObj;
}
}
},
created: function() {
let arr = [];
let titleArr = [];
let r = {};
for (let i = 0; i < this.menuList.length; i++) {
arr.push({
'isActive': false
});
r[this.menuList[i].key] = this.menuList[i].title;
if (this.menuList[i].reflexTitle && this.menuList[i].defaultSelectedIndex > -1) {
titleArr.push({
'title': this.menuList[i].detailList[this.menuList[i].defaultSelectedIndex].title,
'key': this.menuList[i].key
})
} else {
titleArr.push({
'title': this.menuList[i].title,
'key': this.menuList[i].key
})
}
}
this.statusList = arr;
this.titleList = titleArr;
this.tempTitleObj = r;
},
data() {
return {
down: 'sl-down',
up: 'sl-up',
tabHeight: 50,
statusList: [],
selectedIndex: '',
titleList: [],
tempTitleObj: {}
};
},
methods: {
setTitle(obj) {
let list = obj
let titleArr = [];
for (let i = 0; i < list.length; i++) {
if (list[i].reflexTitle && list[i].defaultSelectedIndex > -1) {
titleArr.push({
'title': list[i].detailList[list[i].defaultSelectedIndex].title,
'key': list[i].key
})
} else {
titleArr.push({
'title': list[i].title,
'key': list[i].key
})
}
}
this.titleList = titleArr;
},
refresh() {
let arr = [];
let titleArr = [];
let r = {};
for (let i = 0; i < this.menuList.length; i++) {
arr.push({
'isActive': false
});
r[this.menuList[i].key] = this.menuList[i].title;
if (this.menuList[i].reflexTitle && this.menuList[i].defaultSelectedIndex > -1) {
titleArr.push({
'title': this.menuList[i].detailList[this.menuList[i].defaultSelectedIndex].title,
'key': this.menuList[i].key
})
} else {
titleArr.push({
'title': this.menuList[i].title,
'key': this.menuList[i].key
})
}
}
this.statusList = arr;
this.titleList = titleArr;
this.tempTitleObj = r;
},
getMenuListTemp() {
let arr = this.menuList;
for (let i = 0; i < arr.length; i++) {
let item = arr[i];
for (let j = 0; j < item.detailList.length; j++) {
let d_item = item.detailList[j];
if (j == 0) {
d_item.isSelected = true
} else {
d_item.isSelected = false
}
}
}
return arr;
},
// 重置所有选项包括默认选项并更新result
resetAllSelect(callback) {
this.$refs.slFilterView.resetAllSelect(function(e) {
callback(e);
});
},
// 重置选项为设置的默认值并更新result
resetSelectToDefault(callback) {
this.$refs.slFilterView.resetSelectToDefault(function(e) {
callback(e);
});
},
resetSelect(val, index) {
this.$refs.slFilterView.reset1(val, index);
},
resetMenuList(val) {
this.menuList = val;
this.$emit('update:menuList', val)
this.$forceUpdate();
this.$refs.slFilterView.resetMenuList(val)
},
showMenuClick(index) {
this.selectedIndex = index;
if (this.statusList[index].isActive == true) {
this.$refs.popupRef.close();
this.statusList[index].isActive = false
} else {
this.menuTabClick(index);
this.$refs.popupRef.show()
}
},
menuTabClick(index) {
this.$refs.slFilterView.menuTabClick(index);
for (let i = 0; i < this.statusList.length; i++) {
if (index == i) {
this.statusList[i].isActive = true;
} else {
this.statusList[i].isActive = false;
}
}
},
filterResult(obj) {
let val = obj.result;
let titlesObj = obj.titles;
// 处理选项映射到菜单title
let tempTitle = '';
let tempIndex = 0;
for (let i = 0; i < this.menuList[this.selectedIndex].detailList.length; i++) {
let item = this.menuList[this.selectedIndex].detailList[i];
if (item.value == val[this.menuList[this.selectedIndex].key]) {
tempTitle = item.title;
tempIndex = i;
}
}
if (this.menuList[this.selectedIndex].reflexTitle) {
this.menuList[this.selectedIndex].defaultSelectedIndex = tempIndex;
this.titleList[this.selectedIndex].title = tempTitle;
}
this.$refs.popupRef.close()
if (obj.isReset) {
} else {
this.$emit("result", val)
}
},
close() {
for (let i = 0; i < this.statusList.length; i++) {
this.statusList[i].isActive = false;
}
}
}
}
</script>
<style>
@import 'iconfont/iconfont.css';
.select-tab {
border-bottom: #F7F7F7 1px solid;
background-color: #FFFFFF;
display: flex;
width: 100%;
}
.select-tab-fixed-top {
border-bottom: #F7F7F7 1px solid;
background-color: #FFFFFF;
display: flex;
width: 100%;
position: fixed;
/* #ifdef H5 */
top: 44px;
/* #endif */
/* #ifndef H5 */
top: 0;
/* #endif */
}
.arrows {
margin-left: 5px;
}
.select-tab .select-tab-item,
.select-tab-fixed-top .select-tab-item {
display: flex;
padding-left: 20rpx;
padding-right: 20rpx;
align-items: center;
}
.select-tab .select-tab-item text,
.select-tab-fixed-top .select-tab-item text {
color: #666666;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<view class='refreshBox' :style="isTranform">
<view class='refresh' :style="isZoom" :class="isEnd==2?'animationSmall':''">
<image class='refreshWord' src='../../static/img/public/shuaxin.png' v-if="isEnd == 0"></image>
<view class='refreshCirle animation' v-if="isEnd == 1"></view>
<image class='iconYes' src='../../static/img/public/icon-yes.png' v-if="isEnd==2"></image>
</view>
</view>
</template>
<script>
export default {
name: 'refresh',
props: {
isTop: {
type: Number,
default: 1
}
},
data() {
return {
isTranf: 0,
touchStart: '',
touchMove: '',
isEnd: 0
};
},
methods: {
refreshStart(e) {
if (this.isEnd == 0 && this.isTop == 1) {
this.touchStart = e.changedTouches[0].pageY
}
},
refreshMove(e) {
if (this.isEnd == 0 && this.isTop == 1) {
var touchStart = this.touchStart,
oldMove = this.touchMove,
newMove = e.changedTouches[0].pageY
if (touchStart <= newMove) {
var isTranf = touchStart > newMove ? 0 : newMove - touchStart
this.isTranf = isTranf
this.touchMove = e.changedTouches[0].pageY
}
} else {
this.isTranf = 0
this.isEnd = 0
this.touchStart = 9999
}
},
refreshEnd(e) {
var that = this
if (this.isEnd == 0 && this.isTop == 1) {
if (this.isTranf >= 90) {
this.isTranf = 125
this.isEnd = 1
this.$emit('isRefresh');
} else {
this.isTranf = 0
}
}
},
endAfter() {
this.isEnd = 2
setTimeout(() => {
this.isTranf = 0
this.isEnd = 0
}, 600)
}
},
computed: {
isTranform() {
var isTranf = this.isTranf > 150 ? 150 : this.isTranf
var isTemp = `transform: translateY(${isTranf-100}px);`
return isTemp
},
isZoom() {
var isTranf = this.isTranf > 125 ? 125 : this.isTranf
var isTemp = `zoom:${isTranf/125};`
return isTemp
}
}
}
</script>
<style lang="scss">
.refreshBox {
margin: 0 auto;
width: 100%;
height: 100upx;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
max-height: 300upx;
position: fixed;
z-index: 999;
top: 0;
left: 0;
transform: translateY(-100upx);
}
.animationSmall {
animation: small 1.1s both;
}
@keyframes small {
0% {
transform: scale(1)
}
20% {
transform: scale(1.4)
}
100% {
transform: scale(0)
}
}
.refreshWord {
width: 26upx;
height: 26upx;
border-radius: 26upx;
}
.refresh {
min-width: 50upx;
display: flex;
align-items: center;
justify-content: center;
height: 50upx;
background: #FFFFFF;
box-shadow: 0 0 16upx 0 rgba(0, 0, 0, 0.10);
border-radius: 50upx;
}
.refreshCirle {
width: 26upx;
height: 26upx;
border-radius: 50%;
display: inline-block;
position: relative;
border: 6upx solid black;
border-bottom-color: transparent;
border-top-color: transparent;
}
.animation {
animation: rotate 1.1s infinite;
animation-timing-function: cubic-bezier(0.3, 1.65, 0.7, -0.65);
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.true {
color: black;
}
.iconYes {
width: 34upx;
height: 34upx;
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<view class="navTabBox">
<view class="longTab">
<scroll-view scroll-x="true" style="white-space: nowrap; display: flex" scroll-with-animation
:scroll-left="tabLeft">
<view class="longItem" :style='"width:"+isWidth+"px"' :data-index="index"
:class="index===tabClick?'click':''" v-for="(item,index) in tabTitle" :key="index" :id="'id'+index"
@click="longClick(index)">{{item}}
<text v-if="tabNum[index]!=0 && tabNum.length == tabTitle.length && tabNum[index] != undefined"
style="color: #FFFFFF;font-size: 10rpx;background-color: red; width: 20rpx; height: 20rxp; border-radius: 20rpx;padding-left: 2rpx;padding-right: 2rpx;">{{tabNum[index]}}</text>
</view>
<view class="underlineBox" :style='"transform:translateX("+isLeft+"px);width:"+isWidth+"px"'>
<view class="underline"></view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
name: 'navTab',
props: {
tabTitle: {
type: Array,
default: []
},
tabNum: {
type: Array,
default: []
}
},
data() {
return {
tabClick: 0, //导航栏被点击
isLeft: 0, //导航栏下划线位置
isWidth: 0, //每个导航栏占位
tabLeft: 0,
current: 0 //当前的tab
};
},
created() {
var that = this
// 获取设备宽度
uni.getSystemInfo({
success(e) {
if (that.tabTitle.length <= 5) {
that.isWidth = e.windowWidth / that.tabTitle.length //宽度除以导航标题个数=一个导航所占宽度
} else {
that.isWidth = e.windowWidth / 5
}
}
})
},
methods: {
// 导航栏点击
longClick(index) {
if (this.tabTitle.length > 5) {
var tempIndex = index - 2;
tempIndex = tempIndex <= 0 ? 0 : tempIndex;
this.tabLeft = (index - 2) * this.isWidth //设置下划线位置
}
this.tabClick = index //设置导航点击了哪一个
this.isLeft = index * this.isWidth //设置下划线位置
if (this.current != index) {
this.$emit('onTabClick', index); //设置swiper的第几页
this.current = index
}
}
}
}
</script>
<style lang="scss">
.navTabBox {
width: 100vw;
color: rgba(255, 255, 255, 0.50);
.click {
color: white;
}
.longTab {
width: 100%;
.longItem {
height: 45px;
display: inline-block;
line-height: 45px;
text-align: center;
font-size: 28rpx;
}
.underlineBox {
height: 3px;
width: 20%;
display: flex;
align-content: center;
justify-content: center;
transition: .5s;
.underline {
width: 42upx;
height: 2px;
background-color: white;
}
}
}
}
</style>

View File

@@ -0,0 +1,45 @@
// #ifdef H5
export default {
name: 'Keypress',
props: {
disable: {
type: Boolean,
default: false
}
},
mounted () {
const keyNames = {
esc: ['Esc', 'Escape'],
tab: 'Tab',
enter: 'Enter',
space: [' ', 'Spacebar'],
up: ['Up', 'ArrowUp'],
left: ['Left', 'ArrowLeft'],
right: ['Right', 'ArrowRight'],
down: ['Down', 'ArrowDown'],
delete: ['Backspace', 'Delete', 'Del']
}
const listener = ($event) => {
if (this.disable) {
return
}
const keyName = Object.keys(keyNames).find(key => {
const keyName = $event.key
const value = keyNames[key]
return value === keyName || (Array.isArray(value) && value.includes(keyName))
})
if (keyName) {
// 避免和其他按键事件冲突
setTimeout(() => {
this.$emit(keyName, {})
}, 0)
}
}
document.addEventListener('keyup', listener)
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('keyup', listener)
})
},
render: () => {}
}
// #endif

View File

@@ -0,0 +1,804 @@
<template>
<view class="uni-datetime-picker">
<view @click="initTimePicker">
<slot>
<view class="uni-datetime-picker-timebox uni-datetime-picker-flex"
:class="{'uni-datetime-picker-disabled': disabled}">
{{time}}
<view v-if="!time" class="uni-datetime-picker-time">
选择{{title}}
</view>
<view class="uni-datetime-picker-down-arrow"></view>
</view>
</slot>
</view>
<view v-if="visible" class="uni-datetime-picker-mask" @click="tiggerTimePicker"></view>
<view v-if="visible" class="uni-datetime-picker-popup">
<view class="uni-title">
设置{{title}}
</view>
<picker-view v-show="dateShow" class="uni-datetime-picker-view" :indicator-style="indicatorStyle"
:value="ymd" @change="bindDateChange">
<picker-view-column class="uni-datetime-picker-hyphen">
<view class="uni-datetime-picker-item" v-for="(item,index) in years" :key="index">{{item}}</view>
</picker-view-column>
<picker-view-column class="uni-datetime-picker-hyphen">
<view class="uni-datetime-picker-item" v-for="(item,index) in months" :key="index">
{{item < 10 ? ((item+"").startsWith("0")? item:('0' +item)) : item}}
</view>
</picker-view-column>
<picker-view-column>
<view class="uni-datetime-picker-item" v-for="(item,index) in days" :key="index">
{{item < 10 ? '0' + item : item}}
</view>
</picker-view-column>
</picker-view>
<picker-view v-show="timeShow" class="uni-datetime-picker-view" :indicator-style="indicatorStyle"
:value="hms" @change="bindTimeChange">
<picker-view-column class="uni-datetime-picker-colon">
<view class="uni-datetime-picker-item" v-for="(item,index) in hours" :key="index">
{{item < 10 ? '0' + item : item}}
</view>
</picker-view-column>
<picker-view-column class="uni-datetime-picker-colon">
<view class="uni-datetime-picker-item" v-for="(item,index) in minutes" :key="index">
{{item < 10 ? '0' + item : item}}
</view>
</picker-view-column>
<picker-view-column>
<view class="uni-datetime-picker-item" v-for="(item,index) in seconds" :key="index">
{{item < 10 ? '0' + item : item}}
</view>
</picker-view-column>
</picker-view>
<view class="uni-datetime-picker-btn">
<view class="" @click="clearTime">清空</view>
<view class="uni-datetime-picker-btn-group">
<view class="uni-datetime-picker-cancel" @click="tiggerTimePicker">取消</view>
<view class="" @click="setTime">确定</view>
</view>
</view>
</view>
</view>
</template>
<script>
/**
* DatetimePicker 时间选择器
* @description 可以同时选择日期和时间的选择器
* @tutorial https://ext.dcloud.net.cn/plugin?id=xxx
* @property {String} type = [datetime | date | time] 显示模式
* @property {Boolean} multiple = [true|false] 是否多选
* @property {String|Number} value 默认值
* @property {String|Number} start 起始日期或时间
* @property {String|Number} end 起始日期或时间
* @property {String} return-type = [timestamp | string]
* @event {Function} change 选中发生变化触发
*/
export default {
data() {
return {
indicatorStyle: `height: 50px;`,
visible: false,
dateShow: true,
timeShow: true,
title: '日期和时间',
// 输入框当前时间
time: '',
// 当前的年月日时分秒
year: 1900,
month: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
// 起始时间
startYear: 1920,
startMonth: 1,
startDay: 1,
startHour: 0,
startMinute: 0,
startSecond: 0,
// 结束时间
endYear: 2120,
endMonth: 12,
endDay: 31,
endHour: 23,
endMinute: 59,
endSecond: 59,
}
},
props: {
type: {
type: String,
default: 'datetime'
},
value: {
type: [String, Number],
default: ''
},
start: {
type: [Number, String],
default: ''
},
end: {
type: [Number, String],
default: ''
},
returnType: {
type: String,
default: 'string'
},
disabled: {
type: Boolean,
default: false
}
},
watch: {
value: {
handler(newVal, oldVal) {
if (newVal) {
this.parseValue(this.fixIosDateFormat(newVal)) //兼容 iOS、safari 日期格式
this.initTime()
} else {
this.parseValue(Date.now())
}
},
immediate: true
},
type: {
handler(newValue) {
if (newValue === 'date') {
this.dateShow = true
this.timeShow = false
this.title = '日期'
} else if (newValue === 'time') {
this.dateShow = false
this.timeShow = true
this.title = '时间'
} else {
this.dateShow = true
this.timeShow = true
this.title = '日期和时间'
}
},
immediate: true
},
start: {
handler(newVal) {
this.parseDatetimeRange(this.fixIosDateFormat(newVal), 'start') //兼容 iOS、safari 日期格式
},
immediate: true
},
end: {
handler(newVal) {
this.parseDatetimeRange(this.fixIosDateFormat(newVal), 'end') //兼容 iOS、safari 日期格式
},
immediate: true
},
// 月、日、时、分、秒可选范围变化后,检查当前值是否在范围内,不在则当前值重置为可选范围第一项
months(newVal) {
this.checkValue('month', this.month, newVal)
},
days(newVal) {
this.checkValue('day', this.day, newVal)
},
hours(newVal) {
this.checkValue('hour', this.hour, newVal)
},
minutes(newVal) {
this.checkValue('minute', this.minute, newVal)
},
seconds(newVal) {
this.checkValue('second', this.second, newVal)
}
},
created() {
this.form = this.getForm('uniForms')
this.formItem = this.getForm('uniFormsItem')
if (this.formItem) {
if (this.formItem.name) {
this.rename = this.formItem.name
this.form.inputChildrens.push(this)
}
}
},
computed: {
// 当前年、月、日、时、分、秒选择范围
years() {
return this.getCurrentRange('year')
},
months() {
return this.getCurrentRange('month')
},
days() {
return this.getCurrentRange('day')
},
hours() {
return this.getCurrentRange('hour')
},
minutes() {
return this.getCurrentRange('minute')
},
seconds() {
return this.getCurrentRange('second')
},
// picker 当前值数组
ymd() {
return [this.year - this.minYear, this.month - this.minMonth, this.day - this.minDay]
},
hms() {
return [this.hour - this.minHour, this.minute - this.minMinute, this.second - this.minSecond]
},
// 当前 date 是 start
currentDateIsStart() {
return this.year === this.startYear && this.month === this.startMonth && this.day === this.startDay
},
// 当前 date 是 end
currentDateIsEnd() {
return this.year === this.endYear && this.month === this.endMonth && this.day === this.endDay
},
// 当前年、月、日、时、分、秒的最小值和最大值
minYear() {
return this.startYear
},
maxYear() {
return this.endYear
},
minMonth() {
if (this.year === this.startYear) {
return this.startMonth
} else {
return 1
}
},
maxMonth() {
if (this.year === this.endYear) {
return this.endMonth
} else {
return 12
}
},
minDay() {
if (this.year === this.startYear && this.month === this.startMonth) {
return this.startDay
} else {
return 1
}
},
maxDay() {
if (this.year === this.endYear && this.month === this.endMonth) {
return this.endDay
} else {
return this.daysInMonth(this.year, this.month)
}
},
minHour() {
if (this.type === 'datetime') {
if (this.currentDateIsStart) {
return this.startHour
} else {
return 0
}
}
if (this.type === 'time') {
return this.startHour
}
},
maxHour() {
if (this.type === 'datetime') {
if (this.currentDateIsEnd) {
return this.endHour
} else {
return 23
}
}
if (this.type === 'time') {
return this.endHour
}
},
minMinute() {
if (this.type === 'datetime') {
if (this.currentDateIsStart && this.hour === this.startHour) {
return this.startMinute
} else {
return 0
}
}
if (this.type === 'time') {
if (this.hour === this.startHour) {
return this.startMinute
} else {
return 0
}
}
},
maxMinute() {
if (this.type === 'datetime') {
if (this.currentDateIsEnd && this.hour === this.startHour) {
return this.endMinute
} else {
return 59
}
}
if (this.type === 'time') {
if (this.hour === this.endHour) {
return this.endMinute
} else {
return 59
}
}
},
minSecond() {
if (this.type === 'datetime') {
if (this.currentDateIsStart && this.hour === this.startHour && this.minute === this.startMinute) {
return this.startSecond
} else {
return 0
}
}
if (this.type === 'time') {
if (this.hour === this.startHour && this.minute === this.startMinute) {
return this.startSecond
} else {
return 0
}
}
},
maxSecond() {
if (this.type === 'datetime') {
if (this.currentDateIsEnd && this.hour === this.startHour && this.minute === this.endMinute) {
return this.endSecond
} else {
return 59
}
}
if (this.type === 'time') {
if (this.hour === this.endHour && this.minute === this.endMinute) {
return this.endSecond
} else {
return 59
}
}
}
},
methods: {
/**
* 获取父元素实例
*/
getForm(name = 'uniForms') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
/**
* 解析时分秒字符串例如00:00:00
* @param {String} timeString
*/
parseTimeType(timeString) {
if (timeString) {
let timeArr = timeString.split(':')
this.hour = Number(timeArr[0])
this.minute = Number(timeArr[1])
this.second = Number(timeArr[2])
}
},
/**
* 解析选择器初始值类型可以是字符串、时间戳例如2000-10-02、'08:30:00'、 1610695109000
* @param {String | Number} datetime
*/
initPickerValue(datetime) {
let defaultValue = null
if (datetime) {
defaultValue = this.compareValueWithStartAndEnd(datetime, this.start, this.end)
} else {
defaultValue = Date.now()
defaultValue = this.compareValueWithStartAndEnd(defaultValue, this.start, this.end)
}
this.parseValue(defaultValue)
},
/**
* 初始值规则:
* - 用户设置初始值 value
* - 设置了起始时间 start、终止时间 end并 start < value < end初始值为 value 否则初始值为 start
* - 只设置了起始时间 start并 start < value初始值为 value否则初始值为 start
* - 只设置了终止时间 end并 value < end初始值为 value否则初始值为 end
* - 无起始终止时间,则初始值为 value
* - 无初始值 value则初始值为当前本地时间 Date.now()
* @param {Object} value
* @param {Object} dateBase
*/
compareValueWithStartAndEnd(value, start, end) {
let winner = null
value = this.superTimeStamp(value)
start = this.superTimeStamp(start)
end = this.superTimeStamp(end)
if (start && end) {
if (value < start) {
winner = new Date(start)
} else if (value > end) {
winner = new Date(end)
} else {
winner = new Date(value)
}
} else if (start && !end) {
winner = start <= value ? new Date(value) : new Date(start)
} else if (!start && end) {
winner = value <= end ? new Date(value) : new Date(end)
} else {
winner = new Date(value)
}
return winner
},
/**
* 转换为可比较的时间戳,接受日期、时分秒、时间戳
* @param {Object} value
*/
superTimeStamp(value) {
let dateBase = ''
if (this.type === 'time' && value && typeof value === 'string') {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1
const day = now.getDate()
dateBase = year + '/' + month + '/' + day + ' '
}
if (Number(value) && typeof value !== NaN) {
value = parseInt(value)
dateBase = 0
}
return this.createTimeStamp(dateBase + value)
},
/**
* 解析默认值 value字符串、时间戳
* @param {Object} defaultTime
*/
parseValue(value) {
if (!value) return
if (this.type === 'time' && typeof value === "string") {
this.parseTimeType(value)
} else {
let defaultDate = null
defaultDate = new Date(value)
if (this.type !== 'time') {
this.year = defaultDate.getFullYear()
this.month = defaultDate.getMonth() + 1
this.day = defaultDate.getDate()
}
if (this.type !== 'date') {
this.hour = defaultDate.getHours()
this.minute = defaultDate.getMinutes()
this.second = defaultDate.getSeconds()
}
}
},
/**
* 解析可选择时间范围 start、end年月日字符串、时间戳
* @param {Object} defaultTime
*/
parseDatetimeRange(point, pointType) {
if (point && this.type === 'time') {
const pointArr = point.split(':')
this[pointType + 'Hour'] = Number(pointArr[0])
this[pointType + 'Minute'] = Number(pointArr[1])
this[pointType + 'Second'] = Number(pointArr[2])
} else {
if (!point) {
pointType === 'start' ? this.startYear = this.year - 60 : this.endYear = this.year + 60
return
}
if (Number(point) && Number(point) !== NaN) {
point = parseInt(point)
}
// datetime 的 end 没有时分秒, 则不限制
const hasTime = /[0-9]:[0-9]/
if (this.type === 'datetime' && pointType === 'end' && typeof point === 'string' && !hasTime.test(
point)) {
point = point + ' 23:59:59'
}
// 2016/09/19 17:04:26:719
const split = point.split(" ")
const dataStr = split[0]
const timeStr = split[1]
this[pointType + 'Year'] = dataStr.split("/")[0]
this[pointType + 'Month'] = dataStr.split("/")[1]
this[pointType + 'Day'] = dataStr.split("/")[2]
if (this.type === 'datetime') {
this[pointType + 'Hour'] = timeStr.split(":")[0]
this[pointType + 'Minute'] = timeStr.split(":")[1]
this[pointType + 'Second'] = timeStr.split(":")[2]
}
}
},
// 获取 年、月、日、时、分、秒 当前可选范围
getCurrentRange(value) {
const range = []
for (let i = this['min' + this.capitalize(value)]; i <= this['max' + this.capitalize(value)]; i++) {
range.push(i)
}
return range
},
// 字符串首字母大写
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
// 检查当前值是否在范围内,不在则当前值重置为可选范围第一项
checkValue(name, value, values) {
if (values.indexOf(value) === -1) {
this[name] = values[0]
}
},
// 每个月的实际天数
daysInMonth(year, month) { // Use 1 for January, 2 for February, etc.
return new Date(year, month, 0).getDate();
},
//兼容 iOS、safari 日期格式
fixIosDateFormat(value) {
if (typeof value === 'string') {
value = value.replace(/-/g, '/')
}
return value
},
/**
* 生成时间戳
* @param {Object} time
*/
createTimeStamp(time) {
if (!time) return
if (typeof time === "number") {
return time
} else {
time = time.replace(/-/g, '/')
if (this.type === 'date') {
time = time + ' ' + '00:00:00'
}
return Date.parse(time)
}
},
/**
* 生成日期或时间的字符串
*/
createDomSting() {
const yymmdd = this.year +
'-' +
(this.month < 10 ? '0' + this.month : this.month) +
'-' +
(this.day < 10 ? '0' + this.day : this.day)
const hhmmss = (this.hour < 10 ? '0' + this.hour : this.hour) +
':' +
(this.minute < 10 ? '0' + this.minute : this.minute) +
':' +
(this.second < 10 ? '0' + this.second : this.second)
if (this.type === 'date') {
return yymmdd
} else if (this.type === 'time') {
return hhmmss
} else {
return yymmdd + ' ' + hhmmss
}
},
/**
* 初始化返回值,并抛出 change 事件
*/
initTime() {
this.time = this.createDomSting()
if (this.returnType === 'timestamp' && this.type !== 'time') {
this.formItem && this.formItem.setValue(this.createTimeStamp(this.time))
this.$emit('change', this.createTimeStamp(this.time))
this.$emit('input', this.createTimeStamp(this.time))
} else {
this.formItem && this.formItem.setValue(this.time)
this.$emit('change', this.time)
this.$emit('input', this.time)
}
},
/**
* 用户选择日期或时间更新 data
* @param {Object} e
*/
bindDateChange(e) {
const val = e.detail.value
this.year = this.years[val[0]]
this.month = this.months[val[1]]
this.day = this.days[val[2]]
},
bindTimeChange(e) {
const val = e.detail.value
this.hour = this.hours[val[0]]
this.minute = this.minutes[val[1]]
this.second = this.seconds[val[2]]
},
/**
* 初始化弹出层
*/
initTimePicker() {
if (this.disabled) return
const value = this.fixIosDateFormat(this.value)
this.initPickerValue(value)
this.visible = !this.visible
},
/**
* 触发或关闭弹框
*/
tiggerTimePicker() {
this.visible = !this.visible
},
/**
* 用户点击“清空”按钮,清空当前值
*/
clearTime() {
this.time = ''
this.formItem && this.formItem.setValue(this.time)
this.$emit('change', this.time)
this.$emit('input', this.time)
this.tiggerTimePicker()
},
/**
* 用户点击“确定”按钮
*/
setTime() {
this.initTime()
this.tiggerTimePicker()
}
}
}
</script>
<style>
.uni-datetime-picker-view {
width: 100%;
height: 130px;
margin-top: 30px;
cursor: pointer;
}
.uni-datetime-picker-item {
line-height: 50px;
text-align: center;
}
.uni-datetime-picker-btn {
margin-top: 60px;
display: flex;
justify-content: space-between;
color: #007AFF;
cursor: pointer;
}
.uni-datetime-picker-btn-group {
display: flex;
}
.uni-datetime-picker-cancel {
margin-right: 30px;
}
.uni-datetime-picker-mask {
position: fixed;
bottom: 0px;
top: 0px;
left: 0px;
right: 0px;
background-color: rgba(0, 0, 0, 0.4);
transition-duration: 0.3s;
z-index: 998;
}
.uni-datetime-picker-popup {
border-radius: 8px;
padding: 30px;
width: 270px;
background-color: #fff;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition-duration: 0.3s;
z-index: 999;
}
.uni-datetime-picker-time {
color: grey;
}
.uni-datetime-picker-colon::after {
content: ':';
position: absolute;
top: 53px;
right: 0;
}
.uni-datetime-picker-hyphen::after {
content: '-';
position: absolute;
top: 53px;
right: -2px;
}
.uni-datetime-picker-timebox {
border: 1px solid #E5E5E5;
border-radius: 5px;
padding: 7px 10px;
box-sizing: border-box;
cursor: pointer;
}
// 下箭头
.uni-datetime-picker-down-arrow {
display: inline-block;
position: relative;
width: 20px;
height: 15px;
}
.uni-datetime-picker-down-arrow::after {
display: inline-block;
content: " ";
height: 9px;
width: 9px;
border-width: 0 1px 1px 0;
border-color: #E5E5E5;
border-style: solid;
transform: matrix(0.71, 0.71, -0.71, 0.71, 0, 0);
transform-origin: center;
transition: transform .3s;
position: absolute;
top: 50%;
right: 5px;
margin-top: -5px;
}
.uni-datetime-picker-flex {
display: flex;
justify-content: space-between;
}
.uni-datetime-picker-disabled {
opacity: 0.4;
/* #ifdef H5 */
cursor: not-allowed !important;
/* #endif */
}
</style>

View File

@@ -0,0 +1,243 @@
<template>
<view class="uni-popup-dialog">
<view class="uni-dialog-title">
<text class="uni-dialog-title-text" :class="['uni-popup__'+dialogType]">{{title}}</text>
</view>
<view class="uni-dialog-content">
<text class="uni-dialog-content-text" v-if="mode === 'base'">{{content}}</text>
<input v-else class="uni-dialog-input" v-model="val" type="text" :placeholder="placeholder" :focus="focus">
</view>
<view class="uni-dialog-button-group">
<view class="uni-dialog-button" @click="close">
<text class="uni-dialog-button-text">取消</text>
</view>
<view class="uni-dialog-button uni-border-left" @click="onOk">
<text class="uni-dialog-button-text uni-button-color">确定</text>
</view>
</view>
</view>
</template>
<script>
/**
* PopUp 弹出层-对话框样式
* @description 弹出层-对话框样式
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} value input 模式下的默认值
* @property {String} placeholder input 模式下输入提示
* @property {String} type = [success|warning|info|error] 主题样式
* @value success 成功
* @value warning 提示
* @value info 消息
* @value error 错误
* @property {String} mode = [base|input] 模式、
* @value base 基础对话框
* @value input 可输入对话框
* @property {String} content 对话框内容
* @property {Boolean} beforeClose 是否拦截取消事件
* @event {Function} confirm 点击确认按钮触发
* @event {Function} close 点击取消按钮触发
*/
export default {
name: "uniPopupDialog",
props: {
value: {
type: [String, Number],
default: ''
},
placeholder: {
type: [String, Number],
default: '请输入内容'
},
/**
* 对话框主题 success/warning/info/error 默认 success
*/
type: {
type: String,
default: 'error'
},
/**
* 对话框模式 base/input
*/
mode: {
type: String,
default: 'base'
},
/**
* 对话框标题
*/
title: {
type: String,
default: '提示'
},
/**
* 对话框内容
*/
content: {
type: String,
default: ''
},
/**
* 拦截取消事件 如果拦截取消事件必须监听close事件执行 done()
*/
beforeClose: {
type: Boolean,
default: false
}
},
data() {
return {
dialogType: 'error',
focus: false,
val: ""
}
},
inject: ['popup'],
watch: {
type(val) {
this.dialogType = val
},
mode(val) {
if (val === 'input') {
this.dialogType = 'info'
}
},
value(val) {
this.val = val
}
},
created() {
// 对话框遮罩不可点击
this.popup.mkclick = false
if (this.mode === 'input') {
this.dialogType = 'info'
this.val = this.value
} else {
this.dialogType = this.type
}
},
mounted() {
this.focus = true
},
methods: {
/**
* 点击确认按钮
*/
onOk() {
this.$emit('confirm', () => {
this.popup.close()
if (this.mode === 'input') this.val = this.value
}, this.mode === 'input' ? this.val : '')
},
/**
* 点击取消按钮
*/
close() {
if (this.beforeClose) {
this.$emit('close', () => {
this.popup.close()
})
return
}
this.popup.close()
}
}
}
</script>
<style lang="scss" scoped>
.uni-popup-dialog {
width: 300px;
border-radius: 15px;
background-color: #fff;
}
.uni-dialog-title {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
padding-top: 15px;
padding-bottom: 5px;
}
.uni-dialog-title-text {
font-size: 16px;
font-weight: 500;
}
.uni-dialog-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
padding: 5px 15px 15px 15px;
}
.uni-dialog-content-text {
font-size: 14px;
color: #6e6e6e;
}
.uni-dialog-button-group {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
border-top-color: #f5f5f5;
border-top-style: solid;
border-top-width: 1px;
}
.uni-dialog-button {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
height: 45px;
}
.uni-border-left {
border-left-color: #f0f0f0;
border-left-style: solid;
border-left-width: 1px;
}
.uni-dialog-button-text {
font-size: 14px;
}
.uni-button-color {
color: $uni-color-primary;
}
.uni-dialog-input {
flex: 1;
font-size: 14px;
}
.uni-popup__success {
color: $uni-color-success;
}
.uni-popup__warn {
color: $uni-color-warning;
}
.uni-popup__error {
color: $uni-color-error;
}
.uni-popup__info {
color: #909399;
}
</style>

View File

@@ -0,0 +1,22 @@
export default {
created() {
if (this.type === 'message') {
// 不显示遮罩
this.maskShow = false
// 获取子组件对象
this.childrenMsg = null
}
},
methods: {
customOpen() {
if (this.childrenMsg) {
this.childrenMsg.open()
}
},
customClose() {
if (this.childrenMsg) {
this.childrenMsg.close()
}
}
}
}

View File

@@ -0,0 +1,25 @@
import message from './message.js';
// 定义 type 类型:弹出类型top/bottom/center
const config = {
// 顶部弹出
top: 'top',
// 底部弹出
bottom: 'bottom',
// 居中弹出
center: 'center',
// 消息提示
message: 'top',
// 对话框
dialog: 'center',
// 分享
share: 'bottom',
}
export default {
data() {
return {
config: config
}
},
mixins: [message],
}

View File

@@ -0,0 +1,294 @@
<template>
<view v-if="showPopup" class="uni-popup" :class="[popupstyle]" @touchmove.stop.prevent="clear">
<uni-transition v-if="maskShow" :mode-class="['fade']" :styles="maskClass" :duration="duration"
:show="showTrans" @click="onTap" />
<uni-transition :mode-class="ani" :styles="transClass" :duration="duration" :show="showTrans" @click="onTap">
<view class="uni-popup__wrapper-box" @click.stop="clear">
<slot />
</view>
</uni-transition>
</view>
</template>
<script>
import uniTransition from '../uni-transition/uni-transition.vue'
import popup from './popup.js'
/**
* PopUp 弹出层
* @description 弹出层组件,为了解决遮罩弹层的问题
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [top|center|bottom] 弹出方式
* @value top 顶部弹出
* @value center 中间弹出
* @value bottom 底部弹出
* @value message 消息提示
* @value dialog 对话框
* @value share 底部分享示例
* @property {Boolean} animation = [ture|false] 是否开启动画
* @property {Boolean} maskClick = [ture|false] 蒙版点击是否关闭弹窗
* @event {Function} change 打开关闭弹窗触发e={show: false}
*/
export default {
name: 'UniPopup',
components: {
uniTransition
},
props: {
// 开启动画
animation: {
type: Boolean,
default: true
},
// 弹出层类型可选值top: 顶部弹出层bottom底部弹出层center全屏弹出层
// message: 消息提示 ; dialog : 对话框
type: {
type: String,
default: 'center'
},
// maskClick
maskClick: {
type: Boolean,
default: true
}
},
provide() {
return {
popup: this
}
},
mixins: [popup],
watch: {
/**
* 监听type类型
*/
type: {
handler: function(newVal) {
this[this.config[newVal]]()
},
immediate: true
},
/**
* 监听遮罩是否可点击
* @param {Object} val
*/
maskClick(val) {
this.mkclick = val
}
},
data() {
return {
duration: 300,
ani: [],
showPopup: false,
showTrans: false,
maskClass: {
'position': 'fixed',
'bottom': 0,
'top': 0,
'left': 0,
'right': 0,
'backgroundColor': 'rgba(0, 0, 0, 0.4)'
},
transClass: {
'position': 'fixed',
'left': 0,
'right': 0,
},
maskShow: true,
mkclick: true,
popupstyle: 'top'
}
},
created() {
this.mkclick = this.maskClick
if (this.animation) {
this.duration = 300
} else {
this.duration = 0
}
},
methods: {
clear(e) {
// TODO nvue 取消冒泡
e.stopPropagation()
},
open() {
this.showPopup = true
this.$nextTick(() => {
new Promise(resolve => {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.showTrans = true
// fixed by mehaotian 兼容 app 端
this.$nextTick(() => {
resolve();
})
}, 50);
}).then(res => {
// 自定义打开事件
clearTimeout(this.msgtimer)
this.msgtimer = setTimeout(() => {
this.customOpen && this.customOpen()
}, 100)
this.$emit('change', {
show: true,
type: this.type
})
})
})
},
close(type) {
this.showTrans = false
this.$nextTick(() => {
this.$emit('change', {
show: false,
type: this.type
})
clearTimeout(this.timer)
// 自定义关闭事件
this.customOpen && this.customClose()
this.timer = setTimeout(() => {
this.showPopup = false
}, 300)
})
},
onTap() {
if (!this.mkclick) return
this.close()
},
/**
* 顶部弹出样式处理
*/
top() {
this.popupstyle = 'top'
this.ani = ['slide-top']
this.transClass = {
'position': 'fixed',
'left': 0,
'right': 0,
}
},
/**
* 底部弹出样式处理
*/
bottom() {
this.popupstyle = 'bottom'
this.ani = ['slide-bottom']
this.transClass = {
'position': 'fixed',
'left': 0,
'right': 0,
'bottom': 0
}
},
/**
* 中间弹出样式处理
*/
center() {
this.popupstyle = 'center'
this.ani = ['zoom-out', 'fade']
this.transClass = {
'position': 'fixed',
/* #ifndef APP-NVUE */
'display': 'flex',
'flexDirection': 'column',
/* #endif */
'bottom': 0,
'left': 0,
'right': 0,
'top': 0,
'justifyContent': 'center',
'alignItems': 'center'
}
}
}
}
</script>
<style lang="scss" scoped>
.uni-popup {
position: fixed;
/* #ifndef APP-NVUE */
z-index: 9999;
/* #endif */
}
.uni-popup__mask {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: $uni-bg-color-mask;
opacity: 0;
}
.mask-ani {
transition-property: opacity;
transition-duration: 0.2s;
}
.uni-top-mask {
opacity: 1;
}
.uni-bottom-mask {
opacity: 1;
}
.uni-center-mask {
opacity: 1;
}
.uni-popup__wrapper {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: absolute;
}
.top {
/* #ifdef H5 */
top: var(--window-top);
/* #endif */
/* #ifndef H5 */
top: 0;
/* #endif */
}
.bottom {
bottom: 0;
}
.uni-popup__wrapper-box {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: relative;
/* iphonex 等安全区设置,底部安全区适配 */
/* #ifndef APP-NVUE */
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
/* #endif */
}
.content-ani {
// transition: transform 0.3s;
transition-property: transform, opacity;
transition-duration: 0.2s;
}
.uni-top-content {
transform: translateY(0);
}
.uni-bottom-content {
transform: translateY(0);
}
.uni-center-content {
transform: scale(1);
opacity: 1;
}
</style>

View File

@@ -0,0 +1,280 @@
<template>
<view v-if="isShow" ref="ani" class="uni-transition" :class="[ani.in]"
:style="'transform:' +transform+';'+stylesObject" @click="change">
<slot></slot>
</view>
</template>
<script>
// #ifdef APP-NVUE
const animation = uni.requireNativePlugin('animation');
// #endif
/**
* Transition 过渡动画
* @description 简单过渡动画组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=985
* @property {Boolean} show = [false|true] 控制组件显示或隐藏
* @property {Array} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
* @value fade 渐隐渐出过渡
* @value slide-top 由上至下过渡
* @value slide-right 由右至左过渡
* @value slide-bottom 由下至上过渡
* @value slide-left 由左至右过渡
* @value zoom-in 由小到大过渡
* @value zoom-out 由大到小过渡
* @property {Number} duration 过渡动画持续时间
* @property {Object} styles 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`
*/
export default {
name: 'uniTransition',
props: {
show: {
type: Boolean,
default: false
},
modeClass: {
type: Array,
default () {
return []
}
},
duration: {
type: Number,
default: 300
},
styles: {
type: Object,
default () {
return {}
}
}
},
data() {
return {
isShow: false,
transform: '',
ani: {
in: '',
active: ''
}
};
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.open()
} else {
this.close()
}
},
immediate: true
}
},
computed: {
stylesObject() {
let styles = {
...this.styles,
'transition-duration': this.duration / 1000 + 's'
}
let transfrom = ''
for (let i in styles) {
let line = this.toLine(i)
transfrom += line + ':' + styles[i] + ';'
}
return transfrom
}
},
created() {
// this.timer = null
// this.nextTick = (time = 50) => new Promise(resolve => {
// clearTimeout(this.timer)
// this.timer = setTimeout(resolve, time)
// return this.timer
// });
},
methods: {
change() {
this.$emit('click', {
detail: this.isShow
})
},
open() {
clearTimeout(this.timer)
this.isShow = true
this.transform = ''
this.ani.in = ''
for (let i in this.getTranfrom(false)) {
if (i === 'opacity') {
this.ani.in = 'fade-in'
} else {
this.transform += `${this.getTranfrom(false)[i]} `
}
}
this.$nextTick(() => {
setTimeout(() => {
this._animation(true)
}, 50)
})
},
close(type) {
clearTimeout(this.timer)
this._animation(false)
},
_animation(type) {
let styles = this.getTranfrom(type)
// #ifdef APP-NVUE
if (!this.$refs['ani']) return
animation.transition(this.$refs['ani'].ref, {
styles,
duration: this.duration, //ms
timingFunction: 'ease',
needLayout: false,
delay: 0 //ms
}, () => {
if (!type) {
this.isShow = false
}
this.$emit('change', {
detail: this.isShow
})
})
// #endif
// #ifndef APP-NVUE
this.transform = ''
for (let i in styles) {
if (i === 'opacity') {
this.ani.in = `fade-${type?'out':'in'}`
} else {
this.transform += `${styles[i]} `
}
}
this.timer = setTimeout(() => {
if (!type) {
this.isShow = false
}
this.$emit('change', {
detail: this.isShow
})
}, this.duration)
// #endif
},
getTranfrom(type) {
let styles = {
transform: ''
}
this.modeClass.forEach((mode) => {
switch (mode) {
case 'fade':
styles.opacity = type ? 1 : 0
break;
case 'slide-top':
styles.transform += `translateY(${type?'0':'-100%'}) `
break;
case 'slide-right':
styles.transform += `translateX(${type?'0':'100%'}) `
break;
case 'slide-bottom':
styles.transform += `translateY(${type?'0':'100%'}) `
break;
case 'slide-left':
styles.transform += `translateX(${type?'0':'-100%'}) `
break;
case 'zoom-in':
styles.transform += `scale(${type?1:0.8}) `
break;
case 'zoom-out':
styles.transform += `scale(${type?1:1.2}) `
break;
}
})
return styles
},
_modeClassArr(type) {
let mode = this.modeClass
if (typeof(mode) !== "string") {
let modestr = ''
mode.forEach((item) => {
modestr += (item + '-' + type + ',')
})
return modestr.substr(0, modestr.length - 1)
} else {
return mode + '-' + type
}
},
// getEl(el) {
// console.log(el || el.ref || null);
// return el || el.ref || null
// },
toLine(name) {
return name.replace(/([A-Z])/g, "-$1").toLowerCase();
}
}
}
</script>
<style>
.uni-transition {
transition-timing-function: ease;
transition-duration: 0.3s;
transition-property: transform, opacity;
}
.fade-in {
opacity: 0;
}
.fade-active {
opacity: 1;
}
.slide-top-in {
/* transition-property: transform, opacity; */
transform: translateY(-100%);
}
.slide-top-active {
transform: translateY(0);
/* opacity: 1; */
}
.slide-right-in {
transform: translateX(100%);
}
.slide-right-active {
transform: translateX(0);
}
.slide-bottom-in {
transform: translateY(100%);
}
.slide-bottom-active {
transform: translateY(0);
}
.slide-left-in {
transform: translateX(-100%);
}
.slide-left-active {
transform: translateX(0);
opacity: 1;
}
.zoom-in-in {
transform: scale(0.8);
}
.zoom-out-active {
transform: scale(1);
}
.zoom-out-in {
transform: scale(1.2);
}
</style>