初始化(包含登录模块)

This commit is contained in:
2024-03-29 17:37:48 +08:00
commit 1bcb13ce7a
1306 changed files with 152772 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
import amap from '@/components/amap-wx/lib/amap-wx.js';
// 地铁颜色图
const line = {
'1号线': '#C43B33',
'2号线': '#016299',
'4号线/大兴线': '#008E9C',
'5号线': '#A42380',
'6号线': '#D09900',
'7号线': '#F2C172',
'8号线': '#009D6A',
'9号线': '#8FC41E',
'10号线': '#009DBE',
'13号线': '#F9E701',
'14号线东段': '#D4A7A2',
'14号线西段': '#D4A7A2',
'15号线': '#5D2D69',
'八通线': '#C33A32',
'昌平线': '#DE82B1',
'亦庄线': '#E40177',
'房山线': '#E66021',
'机场线': '#A29BBC',
}
// 150500:地铁站 ,150700:公交站 , 190700:地名地址
const typecode = [{
id: '150500',
icon: 'icon-ditie'
}, {
id: '150700',
icon: 'icon-gongjiao'
}, {
id: '190700',
icon: 'icon-gonglu'
}];
const util = {
key:'b526b09b86cd2996e7732be8ab8c4430',
/**
* 初始化高德地图api
*/
mapInit() {
return new amap.AMapWX({
key: this.key
});
},
// 服务状态吗
typecode,
/**
* 获取地图颜色
*/
lineColor(name) {
if (line[name]) {
return line[name];
} else {
return '#ccc';
}
},
/**
* 关键字颜色变化
*/
serachNmme(val, name) {
let namestr = new RegExp(val);
let nameresult =
`<div style="font-size: 14px;color: #333;line-height: 1.5;">
${name.replace(namestr, "<span style='color:#66ccff;'>" + val + '</span>')}
</div>`
.trim();
return nameresult;
},
/**
* 地址转地铁线路
*/
addressToLine(address, type) {
let addr = address.split(';');
let dt = '';
addr.forEach(elm => {
let color = '#cccccc';
if (type === typecode[0].id) {
color = this.lineColor(elm)
} else if (type === typecode[1].id) {
color = '#4075cb'
}
let style = 'margin:5px 0;margin-right:5px;padding:0 5px;background:' + color +
';font-size:12px;color:#fff;border-radius:3px;';
dt += `<div style=\'${style}\'>${elm}</div>`;
});
return `<div style="display:flex;flex-wrap: wrap;">${dt}</div>`;
},
/**
* 数据处理
*/
dataHandle(item, val) {
// 改变字体颜色
if (val) {
item.nameNodes = util.serachNmme(val, item.name);
} else {
item.nameNodes = `<div style="font-size: 14px;color: #333;line-height: 1.5;">${item.name}</div>`;
}
// 地址解析 地铁
if (
item.typecode === util.typecode[0].id ||
item.typecode === util.typecode[1].id
) {
item.addressNodes = util.addressToLine(item.address, item.typecode);
if (item.typecode === util.typecode[0].id) {
item.icon = util.typecode[0].icon;
} else if (item.typecode === util.typecode[1].id) {
item.icon = util.typecode[1].icon;
}
} else {
item.addressNodes = `<span>${item.district}${
item.address.length > 0 ? '·' + item.address : ''
}</span>`.trim();
item.icon = 'icon-weizhi';
}
if (item.location && item.location.length === 0) {
item.icon = 'icon-sousuo';
}
return item;
},
/**
* 存储历史数据
* val [string | object]需要存储的内容
*/
setHistory(val) {
let searchHistory = uni.getStorageSync('search:history');
if (!searchHistory) searchHistory = [];
let serachData = {};
if (typeof(val) === 'string') {
serachData = {
adcode: [],
address: [],
city: [],
district: [],
id: [],
location: [],
name: val,
typecode: []
};
} else {
serachData = val
}
// 判断数组是否存在,如果存在,那么将放到最前面
for (var i = 0; i < searchHistory.length; i++) {
if (searchHistory[i].name === serachData.name) {
searchHistory.splice(i, 1);
break;
}
}
searchHistory.unshift(util.dataHandle(serachData));
uni.setStorage({
key: 'search:history',
data: searchHistory,
success: function() {
// console.log('success');
}
});
},
getHistory() {
},
removeHistory() {
uni.removeStorage({
key: 'search:history',
success: function(res) {
console.log('success');
}
});
return []
}
}
export default util;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,156 @@
<template>
<view class="uni-padding-wrap">
<page-head :title="title"></page-head>
<button class="button" @click="setTabBarBadge">{{ !hasSetTabBarBadge ? '设置tab徽标' : '移除tab徽标' }}</button>
<button class="button" @click="showTabBarRedDot">{{ !hasShownTabBarRedDot ? '显示红点' : '移除红点'}}</button>
<button class="button" @click="customStyle">{{ !hasCustomedStyle ? '自定义Tab样式' : '移除自定义样式'}}</button>
<button class="button" @click="customItem">{{ !hasCustomedItem ? '自定义Tab信息' : '移除自定义信息' }}</button>
<button class="button" @click="hideTabBar">{{ !hasHiddenTabBar ? '隐藏TabBar' : '显示TabBar' }}</button>
<view class="btn-area">
<button class="button" type="primary" @click="navigateBack">返回上一级</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
title: 'tababr',
hasSetTabBarBadge: false,
hasShownTabBarRedDot: false,
hasCustomedStyle: false,
hasCustomedItem: false,
hasHiddenTabBar: false
}
},
destroyed() {
if (this.hasSetTabBarBadge) {
uni.removeTabBarBadge({
index: 1
})
}
if (this.hasShownTabBarRedDot) {
uni.hideTabBarRedDot({
index: 1
})
}
if (this.hasHiddenTabBar) {
uni.showTabBar()
}
if (this.hasCustomedStyle) {
uni.setTabBarStyle({
color: '#7A7E83',
selectedColor: '#007AFF',
backgroundColor: '#F8F8F8',
borderStyle: 'black'
})
}
if (this.hasCustomedItem) {
let tabBarOptions = {
index: 1,
text: '接口',
iconPath: '/static/api.png',
selectedIconPath: '/static/apiHL.png'
}
uni.setTabBarItem(tabBarOptions)
}
},
methods: {
navigateBack() {
this.$emit('unmount')
},
setTabBarBadge() {
if(this.hasShownTabBarRedDot){
uni.hideTabBarRedDot({
index: 1
})
this.hasShownTabBarRedDot = !this.hasShownTabBarRedDot
}
if (!this.hasSetTabBarBadge) {
uni.setTabBarBadge({
index: 1,
text: '1'
})
} else {
uni.removeTabBarBadge({
index: 1
})
}
this.hasSetTabBarBadge = !this.hasSetTabBarBadge
},
showTabBarRedDot() {
if(this.hasSetTabBarBadge) {
uni.removeTabBarBadge({
index: 1
})
this.hasSetTabBarBadge = !this.hasSetTabBarBadge
}
if (!this.hasShownTabBarRedDot) {
uni.showTabBarRedDot({
index: 1
})
} else {
uni.hideTabBarRedDot({
index: 1
})
}
this.hasShownTabBarRedDot = !this.hasShownTabBarRedDot
},
hideTabBar() {
if (!this.hasHiddenTabBar) {
uni.hideTabBar()
} else {
uni.showTabBar()
}
this.hasHiddenTabBar = !this.hasHiddenTabBar
},
customStyle() {
if (this.hasCustomedStyle) {
uni.setTabBarStyle({
color: '#7A7E83',
selectedColor: '#007AFF',
backgroundColor: '#F8F8F8',
borderStyle: 'black'
})
} else {
uni.setTabBarStyle({
color: '#FFF',
selectedColor: '#007AFF',
backgroundColor: '#000000',
borderStyle: 'black'
})
}
this.hasCustomedStyle = !this.hasCustomedStyle
},
customItem() {
let tabBarOptions = {
index: 1,
text: '接口',
iconPath: '/static/api.png',
selectedIconPath: '/static/apiHL.png'
}
if (this.hasCustomedItem) {
uni.setTabBarItem(tabBarOptions)
} else {
tabBarOptions.text = 'API'
uni.setTabBarItem(tabBarOptions)
}
this.hasCustomedItem = !this.hasCustomedItem
}
}
}
</script>
<style>
.button {
margin-top: 30rpx;
margin-left: 0;
margin-right: 0;
}
.btn-area {
padding-top: 30rpx;
}
</style>

View File

@@ -0,0 +1 @@
export default './lib/marked'

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
/* 下拉刷新区域 */
.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,47 @@
<!-- 下拉刷新区域 -->
<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,96 @@
<!--空布局
可作为独立的组件, 不使用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 }">
<view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view>
<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 lang="scss">
@import '@/style/mixin.scss';
/* 无任何数据的空布局 */
.mescroll-empty {
box-sizing: border-box;
width: 100%;
padding: 100rpx 50rpx;
text-align: center;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
align-items: 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,83 @@
<!-- 回到顶部的按钮 -->
<template>
<image
v-if="mOption.src"
class="mescroll-totop"
:class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': 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="mOption.src"
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 */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.mescroll-totop-safearea {
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,46 @@
/* 上拉加载区域 */
.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-show="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,14 @@
.mescroll-body {
position: relative; /* 下拉刷新区域相对自身定位 */
height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
overflow: hidden; /* 遮住顶部下拉刷新区域 */
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
}
/* 适配 iPhoneX */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.mescroll-safearea {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}

View File

@@ -0,0 +1,323 @@
<template>
<view class="mescroll-body" :style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" @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':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 && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="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>
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
<!-- #ifdef H5 -->
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
<!-- #endif -->
<!-- 适配iPhoneX -->
<view v-if="safearea" class="mescroll-safearea"></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没有更多了,显示END文本提示3没有更多了,不显示END文本提示
isShowEmpty: false, // 是否显示空布局
isShowToTop: false, // 是否显示回到顶部按钮
windowHeight: 0, // 可使用窗口的高度
windowBottom: 0, // 可使用窗口的底部位置
statusBarHeight: 0 // 状态栏高度
};
},
props: {
down: Object, // 下拉刷新的参数配置
up: Object, // 上拉加载的参数配置
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
type: Boolean,
default: true
},
navbar: {
type: Boolean,
default: true
}, // 高度是否减去导航栏和状态栏部分
},
computed: {
// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
minHeight(){
let minHeight = 0;
if(this.height > 0){
minHeight = this.toPx(this.height);
}else if(this.height && Number(this.height) < 0){
minHeight = this.toPx('100%') + uni.upx2px(this.height)
} else {
minHeight = this.toPx('100%');
}
if(this.navbar){
return (minHeight - this.statusBarHeight - uni.upx2px(88) )+ 'px'
}else {
return minHeight + 'px'
}
},
// 下拉布局往下偏移的距离 (px)
numTop() {
return this.toPx(this.top)
},
padTop() {
return this.numTop + 'px';
},
// 上拉布局往上偏移 (px)
numBottom() {
return this.toPx(this.bottom);
},
padBottom() {
return this.numBottom + 'px';
},
// 是否为重置下拉的状态
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(mescroll) {
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
},
// 空布局
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.windowBottom) vm.windowBottom = sys.windowBottom;
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) => {
if(typeof y === 'string'){
// 滚动到指定view (y必须为元素的id,不带#)
setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
uni.createSelectorQuery().select('#'+y).boundingClientRect(function(rect){
let top = rect.top
top += vm.mescroll.getScrollTop()
uni.pageScrollTo({
scrollTop: top,
duration: t
})
}).exec()
},30)
} else{
// 滚动到指定位置 (y必须为数字)
uni.pageScrollTo({
scrollTop: y,
duration: t
})
}
});
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
}
};
</script>
<style>
@import "./mescroll-body.css";
@import "./components/mescroll-down.css";
@import './components/mescroll-up.css';
</style>

View File

@@ -0,0 +1,65 @@
// 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
}
},
// 下拉刷新的回调 (mixin默认resetUpScroll)
downCallback() {
if(this.mescroll.optUp.use){
this.mescroll.resetUpScroll()
}else{
setTimeout(()=>{
this.mescroll.endSuccess();
}, 500)
}
},
// 上拉加载的回调
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: '没有更多了', // 没有更多数据的提示文本
offset: 80, // 距底部多远时,触发upCallback
isBounce: false, // 默认禁止橡皮筋的回弹效果, 必读事项: http://www.mescroll.com/qa.html?v=190725#q25
toTop: {
// 回到顶部按钮,需配置src才显示
src: "http://www.mescroll.com/img/mescroll-totop.png?v=1", // 图片路径 (建议放入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: "http://www.mescroll.com/img/mescroll-empty.png?v=1", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
tip: '~ 空空如也 ~' // 提示
}
}
}
export default GlobalOption

View File

@@ -0,0 +1,32 @@
.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生效 */
}
/* 适配 iPhoneX */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.mescroll-safearea {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}

View File

@@ -0,0 +1,865 @@
/* mescroll
* version 1.2.8
* 2020-06-28 wenju
* http://www.mescroll.com
*/
export default function MeScroll(options, isScrollBody) {
let me = this;
me.version = '1.2.8'; // 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.native) && 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: 1, // 当前页码,默认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 {
let page = this.optUp.page;
page.num = this.startNum; // 重置为第一页
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; // 记录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;
}
/* 设置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 {
this.optUp.page.num += 1;
// 还有下一页
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,396 @@
<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,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-into-view="scrollToViewId" :scroll-with-animation="scrollAnim" @scroll="scroll" @touchstart="touchstartEvent" @touchmove="touchmoveEvent" @touchend="touchendEvent" @touchcancel="touchendEvent" :scroll-y='scrollable' :enable-back-to-top="true">
<!-- 状态栏 -->
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
<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':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 && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="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>
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
<!-- #ifdef H5 -->
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
<!-- #endif -->
<!-- 适配iPhoneX -->
<view v-if="safearea" class="mescroll-safearea"></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,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
downHight: 0, //下拉刷新: 容器高度
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
isShowEmpty: false, // 是否显示空布局
isShowToTop: false, // 是否显示回到顶部按钮
scrollTop: 0, // 滚动条的位置
scrollAnim: false, // 是否开启滚动动画
windowTop: 0, // 可使用窗口的顶部位置
windowBottom: 0, // 可使用窗口的底部位置
windowHeight: 0, // 可使用窗口的高度
statusBarHeight: 0, // 状态栏高度
scrollToViewId: '' // 滚动到指定view的id
}
},
props: {
down: Object, // 下拉刷新的参数配置
up: Object, // 上拉加载的参数配置
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
fixed: { // 是否通过fixed固定mescroll的高度, 默认true
type: Boolean,
default: true
},
height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
type: Boolean,
default: true
}
},
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)
},
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
},
padBottom() {
return !this.isFixed ? this.numBottom + 'px' : 0
},
// 是否为重置下拉的状态
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之外
},
// 列表是否可滑动
scrollable(){
return this.downLoadType===0 || this.isDownReset
},
// 是否在加载中
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 query = uni.createSelectorQuery();
// #ifndef MP-ALIPAY
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
// #endif
let view = query.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组件时,此行不可删)
vm.downResetTimer && clearTimeout(vm.downResetTimer)
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
vm.downLoadType = 0
},300)
},
// 派发下拉刷新的回调
callback: function(mescroll) {
vm.$emit('down', mescroll)
}
},
// 上拉加载的配置
up: {
// 显示加载中的回调
showLoading() {
vm.upLoadType = 1;
},
// 显示无更多数据的回调
showNoMore() {
vm.upLoadType = 2;
},
// 隐藏上拉加载的回调
hideUpScroll(mescroll) {
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
},
// 空布局
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
// #ifdef MP-WEIXIN
// 微信小程序暂不支持slot里面的scroll-into-view,只能计算位置实现
uni.createSelectorQuery().select('#'+vm.viewId).boundingClientRect(function(rect){
let mescrollTop = rect.top // mescroll到顶部的距离
uni.createSelectorQuery().select('#'+y).boundingClientRect(function(rect){
let curY = vm.mescroll.getScrollTop()
let top = rect.top - mescrollTop
top += curY
if(!vm.isFixed) top -= vm.numTop
vm.scrollTop = curY;
vm.$nextTick(function() {
vm.scrollTop = top
})
}).exec()
}).exec()
// #endif
// #ifndef MP-WEIXIN
if (vm.scrollToViewId != y) {
vm.scrollToViewId = y;
} else{
vm.scrollToViewId = ''; // scrollToViewId必须变化才会生效,所以此处先置空再赋值
vm.$nextTick(function(){
vm.scrollToViewId = y;
})
}
// #endif
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 (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
},
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,51 @@
/**
* mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
*/
const MescrollMoreItemMixin = {
// 支付宝小程序不支持props的mixin,需写在具体的页面中
// #ifndef MP-ALIPAY
props:{
i: Number, // 每个tab页的专属下标
index: { // 当前tab的下标
type: Number,
default(){
return 0
}
}
},
// #endif
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;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,139 @@
/* eslint-disable */
var provinceData = [{
"label": "北京市",
"value": "11"
},
{
"label": "天津市",
"value": "12"
},
{
"label": "河北省",
"value": "13"
},
{
"label": "山西省",
"value": "14"
},
{
"label": "内蒙古自治区",
"value": "15"
},
{
"label": "辽宁省",
"value": "21"
},
{
"label": "吉林省",
"value": "22"
},
{
"label": "黑龙江省",
"value": "23"
},
{
"label": "上海市",
"value": "31"
},
{
"label": "江苏省",
"value": "32"
},
{
"label": "浙江省",
"value": "33"
},
{
"label": "安徽省",
"value": "34"
},
{
"label": "福建省",
"value": "35"
},
{
"label": "江西省",
"value": "36"
},
{
"label": "山东省",
"value": "37"
},
{
"label": "河南省",
"value": "41"
},
{
"label": "湖北省",
"value": "42"
},
{
"label": "湖南省",
"value": "43"
},
{
"label": "广东省",
"value": "44"
},
{
"label": "广西壮族自治区",
"value": "45"
},
{
"label": "海南省",
"value": "46"
},
{
"label": "重庆市",
"value": "50"
},
{
"label": "四川省",
"value": "51"
},
{
"label": "贵州省",
"value": "52"
},
{
"label": "云南省",
"value": "53"
},
{
"label": "西藏自治区",
"value": "54"
},
{
"label": "陕西省",
"value": "61"
},
{
"label": "甘肃省",
"value": "62"
},
{
"label": "青海省",
"value": "63"
},
{
"label": "宁夏回族自治区",
"value": "64"
},
{
"label": "新疆维吾尔自治区",
"value": "65"
},
{
"label": "台湾",
"value": "66"
},
{
"label": "香港",
"value": "67"
},
{
"label": "澳门",
"value": "68"
}
]
export default provinceData;

View File

@@ -0,0 +1,230 @@
<template>
<div class="mpvue-picker">
<div :class="{'pickerMask':showPicker}" @click="maskClick" catchtouchmove="true"></div>
<div class="mpvue-picker-content " :class="{'mpvue-picker-view-show':showPicker}">
<div class="mpvue-picker__hd" catchtouchmove="true">
<div class="mpvue-picker__action" @click="pickerCancel">取消</div>
<div class="mpvue-picker__action" :style="{color:themeColor}" @click="pickerConfirm">确定</div>
</div>
<picker-view indicator-style="height: 40px;" class="mpvue-picker-view" :value="pickerValue" @change="pickerChange">
<block>
<picker-view-column>
<div class="picker-item" v-for="(item,index) in provinceDataList" :key="index">{{item.label}}</div>
</picker-view-column>
<picker-view-column>
<div class="picker-item" v-for="(item,index) in cityDataList" :key="index">{{item.label}}</div>
</picker-view-column>
<picker-view-column>
<div class="picker-item" v-for="(item,index) in areaDataList" :key="index">{{item.label}}</div>
</picker-view-column>
</block>
</picker-view>
</div>
</div>
</template>
<script>
import provinceData from './city-data/province.js';
import cityData from './city-data/city.js';
import areaData from './city-data/area.js';
export default {
data() {
return {
pickerValue: [0, 0, 0],
provinceDataList: provinceData,
cityDataList: cityData[0],
areaDataList: areaData[0][0],
/* 是否显示控件 */
showPicker: false,
};
},
created() {
this.init()
},
props: {
/* 默认值 */
pickerValueDefault: {
type: Array,
default () {
return [0, 0, 0]
}
},
/* 主题色 */
themeColor: String
},
watch: {
pickerValueDefault() {
this.init();
}
},
methods: {
init() {
this.handPickValueDefault(); // 对 pickerValueDefault 做兼容处理
const pickerValueDefault = this.pickerValueDefault
this.cityDataList = cityData[pickerValueDefault[0]];
this.areaDataList = areaData[pickerValueDefault[0]][pickerValueDefault[1]];
this.pickerValue = pickerValueDefault;
},
show() {
setTimeout(() => {
this.showPicker = true;
}, 0);
},
maskClick() {
this.pickerCancel();
},
pickerCancel() {
this.showPicker = false;
this._$emit('onCancel');
},
pickerConfirm(e) {
this.showPicker = false;
this._$emit('onConfirm');
},
showPickerView() {
this.showPicker = true;
},
handPickValueDefault() {
const pickerValueDefault = this.pickerValueDefault
let provinceIndex = pickerValueDefault[0]
let cityIndex = pickerValueDefault[1]
const areaIndex = pickerValueDefault[2]
if (
provinceIndex !== 0 ||
cityIndex !== 0 ||
areaIndex !== 0
) {
if (provinceIndex > provinceData.length - 1) {
this.pickerValueDefault[0] = provinceIndex = provinceData.length - 1;
}
if (cityIndex > cityData[provinceIndex].length - 1) {
this.pickerValueDefault[1] = cityIndex = cityData[provinceIndex].length - 1;
}
if (areaIndex > areaData[provinceIndex][cityIndex].length - 1) {
this.pickerValueDefault[2] = areaData[provinceIndex][cityIndex].length - 1;
}
}
},
pickerChange(e) {
let changePickerValue = e.mp.detail.value;
if (this.pickerValue[0] !== changePickerValue[0]) {
// 第一级发生滚动
this.cityDataList = cityData[changePickerValue[0]];
this.areaDataList = areaData[changePickerValue[0]][0];
changePickerValue[1] = 0;
changePickerValue[2] = 0;
} else if (this.pickerValue[1] !== changePickerValue[1]) {
// 第二级滚动
this.areaDataList =
areaData[changePickerValue[0]][changePickerValue[1]];
changePickerValue[2] = 0;
}
this.pickerValue = changePickerValue;
this._$emit('onChange');
},
_$emit(emitName) {
let pickObj = {
label: this._getLabel(),
value: this.pickerValue,
cityCode: this._getCityCode()
};
this.$emit(emitName, pickObj);
},
_getLabel() {
let pcikerLabel =
this.provinceDataList[this.pickerValue[0]].label +
'-' +
this.cityDataList[this.pickerValue[1]].label +
'-' +
this.areaDataList[this.pickerValue[2]].label;
return pcikerLabel;
},
_getCityCode() {
return this.areaDataList[this.pickerValue[2]].value;
}
}
};
</script>
<style>
.pickerMask {
position: fixed;
z-index: 1000;
top: 0;
right: 0;
left: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
}
.mpvue-picker-content {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
transition: all 0.3s ease;
transform: translateY(100%);
z-index: 3000;
}
.mpvue-picker-view-show {
transform: translateY(0);
}
.mpvue-picker__hd {
display: flex;
padding: 9px 15px;
background-color: #fff;
position: relative;
text-align: center;
font-size: 17px;
}
.mpvue-picker__hd:after {
content: ' ';
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 1px;
border-bottom: 1px solid #e5e5e5;
color: #e5e5e5;
transform-origin: 0 100%;
transform: scaleY(0.5);
}
.mpvue-picker__action {
display: block;
flex: 1;
color: #1aad19;
}
.mpvue-picker__action:first-child {
text-align: left;
color: #888;
}
.mpvue-picker__action:last-child {
text-align: right;
}
.picker-item {
text-align: center;
line-height: 40px;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 16px;
}
.mpvue-picker-view {
position: relative;
bottom: 0;
left: 0;
width: 100%;
height: 238px;
background-color: rgba(255, 255, 255, 1);
}
</style>

View File

@@ -0,0 +1,123 @@
<template>
<canvas v-if="canvasId" class="ec-canvas" :id="canvasId" :canvasId="canvasId" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" @error="error"></canvas>
</template>
<script>
import WxCanvas from './wx-canvas';
export default {
props: {
canvasId: {
type: String,
default: 'ec-canvas'
},
lazyLoad: {
type: Boolean,
default: false
},
disableTouch: {
type: Boolean,
default: false
},
throttleTouch: {
type: Boolean,
default: false
}
},
// #ifdef H5
mounted() {
if (!this.lazyLoad) this.init();
},
// #endif
// #ifndef H5
onReady() {
if (!this.lazyLoad) this.init();
},
// #endif
methods: {
setChart(chart){
this.chart = chart
},
init() {
const { canvasId } = this;
this.ctx = wx.createCanvasContext(canvasId, this);
this.canvas = new WxCanvas(this.ctx, canvasId);
const query = wx.createSelectorQuery().in(this);
query
.select(`#${canvasId}`)
.boundingClientRect(res => {
if (!res) {
setTimeout(() => this.init(), 50);
return;
}
this.$emit('onInit', {
width: res.width,
height: res.height
});
})
.exec();
},
canvasToTempFilePath(opt) {
const { canvasId } = this;
this.ctx.draw(true, () => {
wx.canvasToTempFilePath({
canvasId,
...opt
});
});
},
touchStart(e) {
const { disableTouch, chart } = this;
if (disableTouch || !chart || !e.mp.touches.length) return;
const touch = e.mp.touches[0];
chart._zr.handler.dispatch('mousedown', {
zrX: touch.x,
zrY: touch.y
});
chart._zr.handler.dispatch('mousemove', {
zrX: touch.x,
zrY: touch.y
});
},
touchMove(e) {
const { disableTouch, throttleTouch, chart, lastMoveTime } = this;
if (disableTouch || !chart || !e.mp.touches.length) return;
if (throttleTouch) {
const currMoveTime = Date.now();
if (currMoveTime - lastMoveTime < 240) return;
this.lastMoveTime = currMoveTime;
}
const touch = e.mp.touches[0];
chart._zr.handler.dispatch('mousemove', {
zrX: touch.x,
zrY: touch.y
});
},
touchEnd(e) {
const { disableTouch, chart } = this;
if (disableTouch || !chart) return;
const touch = e.mp.changedTouches ? e.mp.changedTouches[0] : {};
chart._zr.handler.dispatch('mouseup', {
zrX: touch.x,
zrY: touch.y
});
chart._zr.handler.dispatch('click', {
zrX: touch.x,
zrY: touch.y
});
}
}
};
</script>
<style scoped>
.ec-canvas {
width: 100%;
height: 100%;
flex: 1;
}
</style>

View File

@@ -0,0 +1,73 @@
export default class WxCanvas {
constructor(ctx, canvasId) {
this.ctx = ctx;
this.canvasId = canvasId;
this.chart = null;
WxCanvas.initStyle(ctx);
this.initEvent();
}
getContext(contextType) {
return contextType === '2d' ? this.ctx : null;
}
setChart(chart) {
this.chart = chart;
}
attachEvent() {
// noop
}
detachEvent() {
// noop
}
static initStyle(ctx) {
const styles = ['fillStyle', 'strokeStyle', 'globalAlpha',
'textAlign', 'textBaseAlign', 'shadow', 'lineWidth',
'lineCap', 'lineJoin', 'lineDash', 'miterLimit', 'fontSize'];
styles.forEach((style) => {
Object.defineProperty(ctx, style, {
set: (value) => {
if ((style !== 'fillStyle' && style !== 'strokeStyle')
|| (value !== 'none' && value !== null)
) {
ctx[`set${style.charAt(0).toUpperCase()}${style.slice(1)}`](value);
}
},
});
});
ctx.createRadialGradient = () => ctx.createCircularGradient(arguments);
}
initEvent() {
this.event = {};
const eventNames = [{
wxName: 'touchStart',
ecName: 'mousedown',
}, {
wxName: 'touchMove',
ecName: 'mousemove',
}, {
wxName: 'touchEnd',
ecName: 'mouseup',
}, {
wxName: 'touchEnd',
ecName: 'click',
}];
eventNames.forEach((name) => {
this.event[name.wxName] = (e) => {
const touch = e.mp.touches[0];
this.chart._zr.handler.dispatch(name.ecName, {
zrX: name.wxName === 'tap' ? touch.clientX : touch.x,
zrY: name.wxName === 'tap' ? touch.clientY : touch.y,
});
};
});
}
}

View File

@@ -0,0 +1,484 @@
<template>
<view class="mpvue-picker">
<view :class="{'pickerMask':showPicker}" @click="maskClick" catchtouchmove="true"></view>
<view class="mpvue-picker-content " :class="{'mpvue-picker-view-show':showPicker}">
<view class="mpvue-picker__hd" catchtouchmove="true">
<view class="mpvue-picker__action" @click="pickerCancel">取消</view>
<view class="mpvue-picker__action" :style="{color:themeColor}" @click="pickerConfirm">确定</view>
</view>
<!-- 单列 -->
<picker-view indicator-style="height: 40px;" class="mpvue-picker-view" :value="pickerValue"
@change="pickerChange" v-if="mode==='selector' && pickerValueSingleArray.length > 0">
<picker-view-column>
<view class="picker-item" v-for="(item,index) in pickerValueSingleArray" :key="index">{{item.label}}
</view>
</picker-view-column>
</picker-view>
<!-- 时间选择器 -->
<picker-view indicator-style="height: 40px;" class="mpvue-picker-view" :value="pickerValue"
@change="pickerChange" v-if="mode==='timeSelector'">
<picker-view-column>
<view class="picker-item" v-for="(item,index) in pickerValueHour" :key="index">{{item.label}}</view>
</picker-view-column>
<picker-view-column>
<view class="picker-item" v-for="(item,index) in pickerValueMinute" :key="index">{{item.label}}
</view>
</picker-view-column>
</picker-view>
<!-- 多列选择 -->
<picker-view indicator-style="height: 40px;" class="mpvue-picker-view" :value="pickerValue"
@change="pickerChange" v-if="mode==='multiSelector'">
<!-- #ifdef VUE3 -->
<template v-for="(n,index) in pickerValueMulArray.length" :key="index">
<picker-view-column>
<view class="picker-item" v-for="(item,index1) in pickerValueMulArray[n]" :key="index1">
{{item.label}}
</view>
</picker-view-column>
</template>
<!-- #endif -->
<!-- #ifndef VUE3 -->
<block v-for="(n,index) in pickerValueMulArray.length" :key="index">
<picker-view-column>
<view class="picker-item" v-for="(item,index1) in pickerValueMulArray[n]" :key="index1">
{{item.label}}
</view>
</picker-view-column>
</block>
<!-- #endif -->
</picker-view>
<!-- 二级联动 -->
<picker-view indicator-style="height: 40px;" class="mpvue-picker-view" :value="pickerValue"
@change="pickerChangeMul" v-if="mode==='multiLinkageSelector' && deepLength===2">
<picker-view-column>
<view class="picker-item" v-for="(item,index) in pickerValueMulTwoOne" :key="index">{{item.label}}
</view>
</picker-view-column>
<picker-view-column>
<view class="picker-item" v-for="(item,index) in pickerValueMulTwoTwo" :key="index">{{item.label}}
</view>
</picker-view-column>
</picker-view>
<!-- 三级联动 -->
<picker-view indicator-style="height: 40px;" class="mpvue-picker-view" :value="pickerValue"
@change="pickerChangeMul" v-if="mode==='multiLinkageSelector' && deepLength===3">
<picker-view-column>
<view class="picker-item" v-for="(item,index) in pickerValueMulThreeOne" :key="index">{{item.label}}
</view>
</picker-view-column>
<picker-view-column>
<view class="picker-item" v-for="(item,index) in pickerValueMulThreeTwo" :key="index">{{item.label}}
</view>
</picker-view-column>
<picker-view-column>
<view class="picker-item" v-for="(item,index) in pickerValueMulThreeThree" :key="index">
{{item.label}}
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
pickerChangeValue: [],
pickerValue: [],
pickerValueArrayChange: true,
modeChange: false,
pickerValueSingleArray: [],
pickerValueHour: [],
pickerValueMinute: [],
pickerValueMulArray: [],
pickerValueMulTwoOne: [],
pickerValueMulTwoTwo: [],
pickerValueMulThreeOne: [],
pickerValueMulThreeTwo: [],
pickerValueMulThreeThree: [],
/* 是否显示控件 */
showPicker: false,
};
},
props: {
/* mode */
mode: {
type: String,
default: 'selector'
},
/* picker 数值 */
pickerValueArray: {
type: Array,
default () {
return []
}
},
/* 默认值 */
pickerValueDefault: {
type: Array,
default () {
return []
}
},
/* 几级联动 */
deepLength: {
type: Number,
default: 2
},
/* 主题色 */
themeColor: String
},
watch: {
pickerValueArray(oldVal, newVal) {
this.pickerValueArrayChange = true;
},
mode(oldVal, newVal) {
this.modeChange = true;
},
pickerValueArray(val) {
this.initPicker(val);
}
},
methods: {
initPicker(valueArray) {
let pickerValueArray = valueArray;
this.pickerValue = this.pickerValueDefault;
// 初始化多级联动
if (this.mode === 'selector') {
this.pickerValueSingleArray = valueArray;
} else if (this.mode === 'timeSelector') {
this.modeChange = false;
let hourArray = [];
let minuteArray = [];
for (let i = 0; i < 24; i++) {
hourArray.push({
value: i,
label: i > 9 ? `${i}` : `0${i}`
});
}
for (let i = 0; i < 60; i++) {
minuteArray.push({
value: i,
label: i > 9 ? `${i}` : `0${i}`
});
}
this.pickerValueHour = hourArray;
this.pickerValueMinute = minuteArray;
} else if (this.mode === 'multiSelector') {
this.pickerValueMulArray = valueArray;
} else if (this.mode === 'multiLinkageSelector' && this.deepLength === 2) {
// 两级联动
let pickerValueMulTwoOne = [];
let pickerValueMulTwoTwo = [];
// 第一列
for (let i = 0, length = pickerValueArray.length; i < length; i++) {
pickerValueMulTwoOne.push(pickerValueArray[i]);
}
// 渲染第二列
// 如果有设定的默认值
if (this.pickerValueDefault.length === 2) {
let num = this.pickerValueDefault[0];
for (
let i = 0, length = pickerValueArray[num].children.length; i < length; i++
) {
pickerValueMulTwoTwo.push(pickerValueArray[num].children[i]);
}
} else {
for (
let i = 0, length = pickerValueArray[0].children.length; i < length; i++
) {
pickerValueMulTwoTwo.push(pickerValueArray[0].children[i]);
}
}
this.pickerValueMulTwoOne = pickerValueMulTwoOne;
this.pickerValueMulTwoTwo = pickerValueMulTwoTwo;
} else if (
this.mode === 'multiLinkageSelector' &&
this.deepLength === 3
) {
let pickerValueMulThreeOne = [];
let pickerValueMulThreeTwo = [];
let pickerValueMulThreeThree = [];
// 第一列
for (let i = 0, length = pickerValueArray.length; i < length; i++) {
pickerValueMulThreeOne.push(pickerValueArray[i]);
}
// 渲染第二列
this.pickerValueDefault =
this.pickerValueDefault.length === 3 ?
this.pickerValueDefault : [0, 0, 0];
if (this.pickerValueDefault.length === 3) {
let num = this.pickerValueDefault[0];
for (
let i = 0, length = pickerValueArray[num].children.length; i < length; i++
) {
pickerValueMulThreeTwo.push(pickerValueArray[num].children[i]);
}
// 第三列
let numSecond = this.pickerValueDefault[1];
for (let i = 0, length = pickerValueArray[num].children[numSecond].children.length; i <
length; i++) {
pickerValueMulThreeThree.push(
pickerValueArray[num].children[numSecond].children[i]
);
}
}
this.pickerValueMulThreeOne = pickerValueMulThreeOne;
this.pickerValueMulThreeTwo = pickerValueMulThreeTwo;
this.pickerValueMulThreeThree = pickerValueMulThreeThree;
}
},
show() {
setTimeout(() => {
if (this.pickerValueArrayChange || this.modeChange) {
this.initPicker(this.pickerValueArray);
this.showPicker = true;
this.pickerValueArrayChange = false;
this.modeChange = false;
} else {
this.showPicker = true;
}
}, 0);
},
maskClick() {
this.pickerCancel();
},
pickerCancel() {
this.showPicker = false;
this._initPickerVale();
let pickObj = {
index: this.pickerValue,
value: this._getPickerLabelAndValue(this.pickerValue, this.mode).value,
label: this._getPickerLabelAndValue(this.pickerValue, this.mode).label
};
this.$emit('onCancel', pickObj);
},
pickerConfirm(e) {
this.showPicker = false;
this._initPickerVale();
let pickObj = {
index: this.pickerValue,
value: this._getPickerLabelAndValue(this.pickerValue, this.mode).value,
label: this._getPickerLabelAndValue(this.pickerValue, this.mode).label
};
this.$emit('onConfirm', pickObj);
},
showPickerView() {
this.showPicker = true;
},
pickerChange(e) {
console.log(11111111, e);
this.pickerValue = e.detail.value;
let pickObj = {
index: this.pickerValue,
value: this._getPickerLabelAndValue(this.pickerValue, this.mode).value,
label: this._getPickerLabelAndValue(this.pickerValue, this.mode).label
};
this.$emit('onChange', pickObj);
},
pickerChangeMul(e) {
if (this.deepLength === 2) {
let pickerValueArray = this.pickerValueArray;
let changeValue = e.detail.value;
// 处理第一列滚动
if (changeValue[0] !== this.pickerValue[0]) {
let pickerValueMulTwoTwo = [];
// 第一列滚动第二列数据更新
for (let i = 0, length = pickerValueArray[changeValue[0]].children.length; i < length; i++) {
pickerValueMulTwoTwo.push(pickerValueArray[changeValue[0]].children[i]);
}
this.pickerValueMulTwoTwo = pickerValueMulTwoTwo;
// 第二列初始化为 0
changeValue[1] = 0;
}
this.pickerValue = changeValue;
} else if (this.deepLength === 3) {
let pickerValueArray = this.pickerValueArray;
let changeValue = e.detail.value;
let pickerValueMulThreeTwo = [];
let pickerValueMulThreeThree = [];
// 重新渲染第二列
// 如果是第一列滚动
if (changeValue[0] !== this.pickerValue[0]) {
this.pickerValueMulThreeTwo = [];
for (let i = 0, length = pickerValueArray[changeValue[0]].children.length; i < length; i++) {
pickerValueMulThreeTwo.push(pickerValueArray[changeValue[0]].children[i]);
}
// 重新渲染第三列
for (let i = 0, length = pickerValueArray[changeValue[0]].children[0].children.length; i <
length; i++) {
pickerValueMulThreeThree.push(pickerValueArray[changeValue[0]].children[0].children[i]);
}
changeValue[1] = 0;
changeValue[2] = 0;
this.pickerValueMulThreeTwo = pickerValueMulThreeTwo;
this.pickerValueMulThreeThree = pickerValueMulThreeThree;
} else if (changeValue[1] !== this.pickerValue[1]) {
// 第二列滚动
// 重新渲染第三列
this.pickerValueMulThreeThree = [];
pickerValueMulThreeTwo = this.pickerValueMulThreeTwo;
for (let i = 0, length = pickerValueArray[changeValue[0]].children[changeValue[1]].children
.length; i <
length; i++) {
pickerValueMulThreeThree.push(pickerValueArray[changeValue[0]].children[changeValue[1]]
.children[
i]);
}
changeValue[2] = 0;
this.pickerValueMulThreeThree = pickerValueMulThreeThree;
}
this.pickerValue = changeValue;
}
let pickObj = {
index: this.pickerValue,
value: this._getPickerLabelAndValue(this.pickerValue, this.mode).value,
label: this._getPickerLabelAndValue(this.pickerValue, this.mode).label
};
this.$emit('onChange', pickObj);
},
// 获取 pxikerLabel
_getPickerLabelAndValue(value, mode) {
let pickerLable;
let pickerGetValue = [];
// selector
if (mode === 'selector') {
pickerLable = this.pickerValueSingleArray[value].label;
pickerGetValue.push(this.pickerValueSingleArray[value].value);
} else if (mode === 'timeSelector') {
pickerLable = `${this.pickerValueHour[value[0]].label}-${this.pickerValueMinute[value[1]].label}`;
pickerGetValue.push(this.pickerValueHour[value[0]].value);
pickerGetValue.push(this.pickerValueHour[value[1]].value);
} else if (mode === 'multiSelector') {
for (let i = 0; i < value.length; i++) {
if (i > 0) {
pickerLable += this.pickerValueMulArray[i][value[i]].label + (i === value.length - 1 ? '' :
'-');
} else {
pickerLable = this.pickerValueMulArray[i][value[i]].label + '-';
}
pickerGetValue.push(this.pickerValueMulArray[i][value[i]].value);
}
} else if (mode === 'multiLinkageSelector') {
/* eslint-disable indent */
pickerLable =
this.deepLength === 2 ?
`${this.pickerValueMulTwoOne[value[0]].label}-${this.pickerValueMulTwoTwo[value[1]].label}` :
`${this.pickerValueMulThreeOne[value[0]].label}-${this.pickerValueMulThreeTwo[value[1]].label}-${this.pickerValueMulThreeThree[value[2]].label}`;
if (this.deepLength === 2) {
pickerGetValue.push(this.pickerValueMulTwoOne[value[0]].value);
pickerGetValue.push(this.pickerValueMulTwoTwo[value[1]].value);
} else {
pickerGetValue.push(this.pickerValueMulThreeOne[value[0]].value);
pickerGetValue.push(this.pickerValueMulThreeTwo[value[1]].value);
pickerGetValue.push(this.pickerValueMulThreeThree[value[2]].value);
}
/* eslint-enable indent */
}
return {
label: pickerLable,
value: pickerGetValue
};
},
// 初始化 pickerValue 默认值
_initPickerVale() {
if (this.pickerValue.length === 0) {
if (this.mode === 'selector') {
this.pickerValue = [0];
} else if (this.mode === 'multiSelector') {
this.pickerValue = new Int8Array(this.pickerValueArray.length);
} else if (
this.mode === 'multiLinkageSelector' &&
this.deepLength === 2
) {
this.pickerValue = [0, 0];
} else if (
this.mode === 'multiLinkageSelector' &&
this.deepLength === 3
) {
this.pickerValue = [0, 0, 0];
}
}
}
}
};
</script>
<style>
.pickerMask {
position: fixed;
z-index: 1000;
top: 0;
right: 0;
left: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
}
.mpvue-picker-content {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
transition: all 0.3s ease;
transform: translateY(100%);
z-index: 3000;
}
.mpvue-picker-view-show {
transform: translateY(0);
}
.mpvue-picker__hd {
display: flex;
padding: 9px 15px;
background-color: #fff;
position: relative;
text-align: center;
font-size: 17px;
}
.mpvue-picker__hd:after {
content: ' ';
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 1px;
border-bottom: 1px solid #e5e5e5;
color: #e5e5e5;
transform-origin: 0 100%;
transform: scaleY(0.5);
}
.mpvue-picker__action {
display: block;
flex: 1;
color: #1aad19;
}
.mpvue-picker__action:first-child {
text-align: left;
color: #888;
}
.mpvue-picker__action:last-child {
text-align: right;
}
.picker-item {
text-align: center;
line-height: 40px;
font-size: 16px;
}
.mpvue-picker-view {
position: relative;
bottom: 0;
left: 0;
width: 100%;
height: 238px;
background-color: rgba(255, 255, 255, 1);
}
</style>

View File

@@ -0,0 +1,175 @@
class GestureLock {
constructor(containerWidth, cycleRadius) {
this.containerWidth = containerWidth; // 容器宽度
this.cycleRadius = cycleRadius; // 圆的半径
this.circleArray = []; // 全部圆的对象数组
this.checkPoints = []; // 选中的圆的对象数组
this.lineArray = []; // 已激活锁之间的线段数组
this.lastCheckPoint = 0; // 最后一个激活的锁
this.offsetX = 0; // 容器的 X 偏移
this.offsetY = 0; // 容器的 Y 偏移
this.activeLine = {}; // 最后一个激活的锁与当前位置之间的线段
this.windowWidth = wx.getSystemInfoSync().windowWidth; // 窗口大小(用于rpx 和 px 转换)
this.initCircleArray();
}
// 初始化 画布上的 9个圆
initCircleArray() {
const cycleMargin = (this.containerWidth - 6 * this.cycleRadius) / 6;
let count = 0;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
count++;
this.circleArray.push({
count: count,
x: this.rpxTopx((cycleMargin + this.cycleRadius) * (j * 2 + 1)),
y: this.rpxTopx((cycleMargin + this.cycleRadius) * (i * 2 + 1)),
radius: this.rpxTopx(this.cycleRadius),
check: false,
style: {
left: (cycleMargin + this.cycleRadius) * (j * 2 + 1) - this.cycleRadius + 'rpx',
top: (cycleMargin + this.cycleRadius) * (i * 2 + 1) - this.cycleRadius + 'rpx',
width: this.cycleRadius * 2 + 'rpx',
}
});
}
}
}
onTouchStart(e) {
this.setOffset(e);
this.checkTouch({
x: e.touches[0].pageX - this.offsetX,
y: e.touches[0].pageY - this.offsetY
});
}
onTouchMove(e) {
this.moveDraw(e)
}
onTouchEnd(e) {
const checkPoints = this.checkPoints;
this.reset();
return checkPoints;
}
// 初始化 偏移量
setOffset(e) {
this.offsetX = e.currentTarget.offsetLeft;
this.offsetY = e.currentTarget.offsetTop;
}
// 检测当时 触摸位置是否位于 锁上
checkTouch({
x,
y
}) {
for (let i = 0; i < this.circleArray.length; i++) {
let point = this.circleArray[i];
if (this.isPointInCycle(x, y, point.x, point.y, point.radius)) {
if (!point.check) {
this.checkPoints.push(point.count);
if (this.lastCheckPoint != 0) {
// 已激活锁之间的线段
const line = this.drawLine(this.lastCheckPoint, point);
this.lineArray.push(line);
}
this.lastCheckPoint = point;
}
point.check = true;
return;
}
}
}
// 画线 - 返回 样式 对象
drawLine(start, end) {
const width = this.getPointDis(start.x, start.y, end.x, end.y);
const rotate = this.getAngle(start, end);
return {
activeLeft: start.x + 'px',
activeTop: start.y + 'px',
activeWidth: width + 'px',
activeRotate: rotate + 'deg'
}
}
// 获取 画线的 角度
getAngle(start, end) {
var diff_x = end.x - start.x,
diff_y = end.y - start.y;
if (diff_x >= 0) {
return 360 * Math.atan(diff_y / diff_x) / (2 * Math.PI);
} else {
return 180 + 360 * Math.atan(diff_y / diff_x) / (2 * Math.PI);
}
}
// 判断 当前点是否位于 锁内
isPointInCycle(x, y, circleX, circleY, radius) {
return (this.getPointDis(x, y, circleX, circleY) < radius) ? true : false;
}
// 获取两点之间距离
getPointDis(ax, ay, bx, by) {
return Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
}
// 移动 绘制
moveDraw(e) {
// 画经过的圆
const x = e.touches[0].pageX - this.offsetX;
const y = e.touches[0].pageY - this.offsetY;
this.checkTouch({
x,
y
});
// 画 最后一个激活的锁与当前位置之间的线段
this.activeLine = this.drawLine(this.lastCheckPoint, {
x,
y
});
}
// 使 画布 恢复初始状态
reset() {
this.circleArray.forEach((item) => {
item.check = false;
});
this.checkPoints = [];
this.lineArray = [];
this.activeLine = {};
this.lastCheckPoint = 0;
}
// 获取 最后一个激活的锁与当前位置之间的线段
getActiveLine() {
return this.activeLine;
}
// 获取 圆对象数组
getCycleArray() {
return this.circleArray;
}
// 获取 已激活锁之间的线段
getLineArray() {
return this.lineArray;
}
// 将 RPX 转换成 PX
rpxTopx(rpx) {
return rpx / 750 * this.windowWidth;
}
}
export default GestureLock;

View File

@@ -0,0 +1,138 @@
<template>
<view class="gesture-lock" :class="{error:error}" :style="{width: containerWidth +'rpx', height:containerWidth +'rpx'}"
@touchstart.stop="onTouchStart" @touchmove.stop="onTouchMove" @touchend.stop="onTouchEnd">
<!-- 同级 v-for key 重复会有问题需要套一层 -->
<!-- 9 个圆 -->
<view>
<view v-for="(item,i) in circleArray" :key="i" class="cycle" :class="{check:item.check}" :style="{left:item.style.left,top:item.style.top,width:item.style.width,height:item.style.width}">
</view>
</view>
<view>
<!-- 已激活锁之间的线段 -->
<view v-for="(item,i) in lineArray" :key="i" class="line" :style="{left:item.activeLeft,top:item.activeTop,width:item.activeWidth,'-webkit-transform':'rotate('+item.activeRotate+')',transform:'rotate('+item.activeRotate+')'}">
</view>
</view>
<!-- 最后一个激活的锁与当前位置之间的线段 -->
<view class="line" :style="{left:activeLine.activeLeft,top:activeLine.activeTop,width:activeLine.activeWidth,'-webkit-transform':'rotate('+activeLine.activeRotate+')',transform:'rotate('+activeLine.activeRotate+')'}">
</view>
</view>
</template>
<script>
import GestureLock from './gestureLock';
export default {
name: 'index',
props: {
/**
* 容器宽度
*/
containerWidth: {
type: [Number, String],
default: 0
},
/**
* 圆的半径
*/
cycleRadius: {
type: [Number, String],
default: 0
},
/**
* 已设定的密码
*/
password: {
type: Array,
default () {
return []
}
},
},
data() {
return {
gestureLock: {}, // 锁对象
circleArray: [], // 圆对象数组
lineArray: [], // 已激活锁之间的线段
activeLine: {}, // 最后一个激活的锁与当前位置之间的线段
error: false
}
},
methods: {
onTouchStart(e) {
this.gestureLock.onTouchStart(e);
this.refesh();
},
onTouchMove(e) {
this.gestureLock.onTouchMove(e);
this.refesh();
},
onTouchEnd(e) {
const checkPoints = this.gestureLock.onTouchEnd(e);
if (!this.password.length || checkPoints.join('') == this.password.join('')) {
this.refesh();
this.$emit('end', checkPoints);
} else {
this.error = true;
setTimeout(() => {
this.refesh();
this.$emit('end', checkPoints);
}, 800);
}
},
refesh() {
this.error = false;
this.circleArray = this.gestureLock.getCycleArray();
this.lineArray = this.gestureLock.getLineArray();
this.activeLine = this.gestureLock.getActiveLine();
}
},
mounted() {
this.gestureLock = new GestureLock(this.containerWidth, this.cycleRadius);
this.refesh();
}
}
</script>
<style scoped>
.gesture-lock {
margin: 0 auto;
position: relative;
box-sizing: border-box;
overflow: auto;
}
.gesture-lock .cycle {
box-sizing: border-box;
position: absolute;
border: 2px solid #66aaff;
border-radius: 50%;
}
.gesture-lock .cycle.check:after {
content: "";
display: block;
position: absolute;
width: 32%;
height: 32%;
border: 2px solid #66aaff;
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.gesture-lock .line {
height: 0;
border-top: 2px solid #66aaff;
position: absolute;
transform-origin: left center;
}
.gesture-lock.error .cycle.check,
.gesture-lock.error .cycle.check:after,
.gesture-lock.error .line {
border-color: #ffa197;
}
</style>

362
components/music.vue Normal file
View File

@@ -0,0 +1,362 @@
<template>
<view>
<view :class="['fuchuang',showBig?'bigMode':'miniMode']" v-show="userInfo.playVisible">
<view>
<view class="audo-video" >
<!--音频播放按钮处-->
<view class="audo-top">
<!-- 播放封面 -->
<image @click="changeShow" style="width: 120rpx; height: 120rpx; margin-top: 0;" :class="['fengImg','fengmianBox','defaultBg', userInfo.playFlag ? 'playAnimate' : '']" :src="userInfo.fengImg" mode="aspectFill"></image>
<!-- <image v-else @click="changeShow" style="width: 120rpx; height: 120rpx; margin-top: 0;" :class="['fengImg','fengmianBox','defaultBg', userInfo.playFlag ? 'playAnimate' : '']" src="@/static/icon/fengziIcon.jpg" mode="aspectFill"></image> -->
<!-- <u-icon name="arrow-right" color="#61e781" size="28" v-else @click="changeShow"></u-icon> -->
<!--上一首切换按钮-->
<!-- <image class="prevMusic" @click="prevMusic" src="/static/xys.png"
mode="aspectFill"></image> -->
<!-- <image v-else style="width:50rpx;height:50rpx;" @click="nosig" src="/static/sys.png" mode="aspectFill"></image> -->
<!--上一首切换按钮-->
<!--快退按钮-->
<!-- <image src="/static/kt.png" style="width:45rpx;height:45rpx;" mode="aspectFill" @click="kt()"></image> -->
<!--快退按钮-->
<!--播放按钮-->
<image class="plays" :src="userInfo.playFlag ?'/static/zantigBtn.png':'/static/bofangBtn.png'" mode="aspectFill"
style="" @click.stop="plays()"></image>
<!--播放按钮-->
<!--快进按钮-->
<!-- <image src="/static/kj.png" style="width:45rpx;height:45rpx;" mode="aspectFill" @click="kj()"></image> -->
<!--快进按钮-->
<!--下一首切换按钮-->
<!-- <image v-if="jia" @click="noxig" style="width:50rpx;height:50rpx;transform:rotate(180deg)" src="/static/sys.png"
mode="aspectFill"> </image> -->
<!-- <image class="nextMusic" style="" src="/static/xys.png" @click="nextMusic" mode="aspectFill"></image> -->
<!-- <u-icon name="arrow-right" color="#61e781" size="28" v-if="!showBig" @click="changeShow"></u-icon> -->
<!-- 暂时去掉关闭按钮 -->
<u-icon name="close" color="#61e781" size="20" style="background-color: #fff;border-radius: 100%;" v-if="!showBig" @click="closePlayer"></u-icon>
<!-- 暂时去掉关闭按钮 -->
<!-- 播放目录 -->
<!-- <image src="/static/libIcon.png" style="width:45rpx;height:45rpx;" mode="aspectFill"></image> -->
</view>
</view>
</view>
</view>
<view class="h-100"></view>
<!--占位-->
</view>
</template>
<script>
import {
mapState,mapMutations
} from 'vuex';
export default {
name:"music",
props:{
playData:{
type:Object,
default:()=>({})
},
},
data() {
return {
showBig:false, // 显示详细模式
muteBgMusic:true,
fengImg:'',
libLIst:[], // 播放目录
playIndex: 0,// 播放器index
};
},
onLoad() {
this.$music.playBgm({mute:false})
 
               
},
created() {
// this.fengImg = this.$music.getCoverImg()
this.fengImg = this.userInfo.fengImg
this.libLIst = this.userInfo.myList
},
mounted() {
},
methods:{
...mapMutations(['setUserInfo']),
closePlayer(){
// 关闭播放器
// console.log('点击了关闭按钮')
this.$music.setCloseBgm() // 关闭音频
uni.setStorage({
key: 'playVisible',
data: false,
success: function () {
console.log('success');
}
});
this.setUserInfo({'playVisible':false})
},
changeShow(){
uni.navigateTo({
url:'/pages/listen/bigListen'
})
// this.showBig = !this.showBig
},
// 上一首
prevMusic(){
if(this.$bgm._options.src == ''){ // 如果直接点下一首,没点播放
this.$music.playBgm({mute:false})
this.$music.setPlayIndex('next')
}else{
this.$music.setPlayIndex('prev')
}
},
nextMusic(){ // 下一首
if(this.$bgm._options.src == ''){ // 如果直接点下一首,没点播放
this.$music.playBgm({mute:false})
this.$music.setPlayIndex('next')
}else{
this.$music.setPlayIndex('next')
}
},
//关闭或开启 音乐
plays() {
this.muteBgMusic = !this.muteBgMusic
console.log(this.muteBgMusic,this.muteBgMusic?'已关闭音乐####':'已开启音乐####');
if (this.userInfo.playFlag) {
// 暂停
// this.$music.playBgm({mute:true})
this.$bgm.pause()
} else {
// 播放
// this.$music.playBgm({mute:false})
if(this.$bgm._options.src == ''){
this.$music.playBgm({mute:false})
}else{
this.$bgm.play()
}
}
},
},
computed:{
...mapState(['userInfo']),
timer() {
return calcTimer(this.userInfo.currentTime)
},
overTimer() {
return calcTimer(this.userInfo.duration)
},
playStatus(){
var playFlag = false
this.userInfo.playFlag !== undefined ? playFlag = this.userInfo.playFlag : ''
console.log(playFlag,'playFlag')
return playFlag
}
},
watch:{
playData(newVal, oldVal){
console.log(newVal,'组件获取到新值')
if(newVal.myList.length > 0){
this.setUserInfo({'playIndex': 0})
this.$music.setList(newVal.myList,'autoPlay')
// this.fengImg = newVal.fengImg
// 本地存储播放列表
uni.setStorage({
key: 'playData',
data: newVal,
success: function () {
console.log('success');
}
});
// 系统暂存
this.setUserInfo({'myList':newVal.myList})
this.libLIst = newVal.myList
// console.log(newVal.myList,'newVal.myList')
uni.showToast({
title:'添加列表成功',
icon:'success',
duration:2000
})
}else{
uni.showToast({
title:'添加列表失败',
icon:'error',
duration:2000
})
}
}
}
}
//时间换算
function calcTimer(timer) {
if (timer === 0 || typeof timer !== 'number') {
return '00:00'
}
let mm = Math.floor(timer / 60)
let ss = Math.floor(timer % 60)
if (mm < 10) {
mm = '0' + mm
}
if (ss < 10) {
ss = '0' + ss
}
return mm + ':' + ss
}
</script>
<style lang="scss" scoped>
.fengImg{ border-radius: 100%; background-size: cover; }
@-webkit-keyframes rotation {
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
}
}
.leveOne{padding: 0 20rpx; }
.playList{ width: calc(100% - 250rpx); height: 300rpx; overflow-y: scroll;}
.playList .item{line-height:80rpx; white-space: nowrap;
overflow-x: hidden; font-size: 38rpx;
text-overflow: ellipsis;}
.playNow{color: #27b386;}
.flexbox{display: flex; }
.miniMode{width:160px; padding-left: -20px; border-radius: 100rpx 0 0 100rpx; padding-top: 0rpx;
height: 140rpx;
border: 1px solid #eee;
.closeBtn{border: 1px solid #666; display: inline-block; padding: 3px;}
.leveOne{display: none;}
.prevMusic{ display: none;
width:30rpx;height:30rpx;transform:rotate(180deg)
}
.plays{width:50rpx;height:50rpx; margin-left: 0 !important;}
.nextMusic{width:50rpx;height:50rpx; display: none;}
}
.fuchuang{position: fixed; padding-right: 10px; padding-left: 0; bottom:180rpx; right:0; z-index: 888; background-color:rgba(255, 255, 255, 1); }
.playAnimate{
-webkit-transform: rotate(360deg);
animation: rotation 6s linear infinite;
-moz-animation: rotation 6s linear infinite;
-webkit-animation: rotation 6s linear infinite;
-o-animation: rotation 6s linear infinite;
}
.playNow{color: #27b386;}
.fengmianBox{text-align: center; margin-top:50rpx;
.times{ }
}
.fengmianBox .defaultBg{ width: 100%; margin: 0 auto;border-radius: 200rpx; margin: 0 auto;
margin-bottom: 20rpx; background-size: cover;
background-repeat: no-repeat;
background-image: url('@/static/icon/home_icon_0.png');
}
page {
background-color: #F6F6F8;
}
/* #video {
width: 100%;
} */
.audo-video {
padding-bottom: 20rpx;
color: #999;
}
.slider-box {
display: flex;
align-items: center;
justify-content: center;
font-size: 27rpx;
color: #999; margin: 0 auto;
}
button {
display: inline-block;
width: 100rpx;
background-color: #fff;
font-size: 24rpx;
color: #000;
padding: 0;
}
.hidden {
position: fixed;
top: 0;
left: -10rpx;
z-index: -1;
width: 1rpx;
height: 1rpx;
}
.audo-top {
padding: 10rpx;
display: flex;
justify-content: space-between;
align-items: center;
image {
width: 45rpx;
height: 45rpx;
}
}
.audo-a {
display: flex;
justify-content: space-between;
align-items: center;
width: 750rpx;
position: relative;
z-index: 9; margin: 0 auto;
}
.beishu {
position: relative;
width: 100rpx;
padding-top: 5rpx;
padding-bottom: 5rpx;
text-align: center;
border-radius: 25rpx;
font-size: 28rpx;
}
.absolute {
position: absolute;
.beishu-a {
width: 200rpx;
border-radius: 20rpx;
text-align: center;
line-height: 90rpx;
background: #fff;
.title {
pdding-left: 30rpx;
}
}
}
</style>

View File

@@ -0,0 +1,38 @@
<template name="page-foot">
<view class="page-share-title">
<text>感谢{{name}}提供本示例</text>
<text class="submit" @click="submit">我也提交</text>
</view>
</template>
<script>
export default {
name: "page-foot",
props: {
name: {
type: String,
default: ""
}
},
methods: {
submit() {
uni.showModal({
content:"hello uni-app开源地址为https://github.com/dcloudio/uni-app/tree/master/examples请在这个开源项目上贡献你的代码",
showCancel:false
})
}
}
}
</script>
<style>
.page-share-title {
text-align: center;
font-size: 30rpx;
color: #BEBEBE;
padding: 20rpx 0;
}
.submit {
border-bottom: 1rpx solid #BEBEBE;
}
</style>

View File

@@ -0,0 +1,16 @@
<template name="page-head">
<view class="common-page-head">
<view class="common-page-head-title">{{title}}</view>
</view>
</template>
<script>
export default {
name: "page-head",
props: {
title: {
type: String,
default: ""
}
}
}
</script>

66
components/product.vue Normal file
View File

@@ -0,0 +1,66 @@
<template>
<view class="product">
<image class="product-image" :src="image ? image : 'https://via.placeholder.com/150x200'"></image>
<view class="product-title">{{title}}</view>
<view class="product-price">
<text class="product-price-favour">{{originalPrice}}</text>
<text class="product-price-original">{{favourPrice}}</text>
<text class="product-tip">{{tip}}</text>
</view>
</view>
</template>
<script>
export default {
name: 'product',
props: ['image', 'title', 'originalPrice', 'favourPrice', 'tip']
}
</script>
<style>
.product {
padding: 10rpx 20rpx;
display: flex;
flex-direction: column;
}
.product-image {
height: 330rpx;
width: 330rpx;
}
.product-title {
width: 300rpx;
font-size: 32rpx;
word-break: break-all;
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.product-price {
font-size: 28rpx;
position: relative;
}
.product-price-original {
color: #E80080;
}
.product-price-favour {
color: #888888;
text-decoration: line-through;
margin-left: 10rpx;
}
.product-tip {
position: absolute;
right: 10rpx;
background-color: #FF3333;
color: #FFFFFF;
padding: 0 10rpx;
border-radius: 5rpx;
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<view>
<!-- 加载动画组件 -->
<z-loading></z-loading>
<!-- #ifdef MP-WEIXIN -->
<!-- 小程序绑定微信头像昵称弹窗组件 -->
<applets-bind-userInfo></applets-bind-userInfo>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<guide-pages></guide-pages>
<!-- #endif -->
</view>
</template>
<script>
export default {
};
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,175 @@
<template>
<view class="view">
<view class="list-cell view" hover-class="uni-list-cell-hover" @click="bindClick">
<view class="media-list view" v-if="options.title">
<view class="view" :class="{'media-image-right': options.article_type === 2, 'media-image-left': options.article_type === 1}">
<text class="media-title" :class="{'media-title2': options.article_type === 1 || options.article_type === 2}">{{options.title}}</text>
<view v-if="options.image_list || options.image_url" class="image-section view" :class="{'image-section-right': options.article_type === 2, 'image-section-left': options.article_type === 1}">
<image class="image-list1" :class="{'image-list2': options.article_type === 1 || options.article_type === 2}"
v-if="options.image_url" :src="options.image_url"></image>
<image class="image-list3" v-if="options.image_list" :src="source.url" v-for="(source, i) in options.image_list"
:key="i" />
</view>
</view>
<view class="media-foot view">
<view class="media-info view">
<text class="info-text">{{options.source}}</text>
<text class="info-text">{{options.comment_count}}条评论</text>
<text class="info-text">{{options.datetime}}</text>
</view>
<view class="max-close-view view" @click.stop="close">
<view class="close-view view"><text class="close">×</text></view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
options: {
type: Object,
default: function(e) {
return {}
}
}
},
methods: {
close(e) {
this.$emit('close');
},
bindClick() {
this.$emit('click');
}
}
}
</script>
<style>
.view {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.list-cell {
width: 750rpx;
padding: 0 30rpx;
}
.uni-list-cell-hover {
background-color: #eeeeee;
}
.media-list {
flex: 1;
flex-direction: column;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #c8c7cc;
padding: 20rpx 0;
}
.media-image-right {
flex-direction: row;
}
.media-image-left {
flex-direction: row-reverse;
}
.media-title {
flex: 1;
}
.media-title {
lines: 3;
text-overflow: ellipsis;
font-size: 32rpx;
color: #555555;
}
.media-title2 {
flex: 1;
margin-top: 6rpx;
line-height: 40rpx;
}
.image-section {
margin-top: 20rpx;
flex-direction: row;
justify-content: space-between;
}
.image-section-right {
margin-top: 0rpx;
margin-left: 10rpx;
width: 225rpx;
height: 146rpx;
}
.image-section-left {
margin-top: 0rpx;
margin-right: 10rpx;
width: 225rpx;
height: 146rpx;
}
.image-list1 {
width: 690rpx;
height: 481rpx;
}
.image-list2 {
width: 225rpx;
height: 146rpx;
}
.image-list3 {
width: 225rpx;
height: 146rpx;
}
.media-info {
flex-direction: row;
}
.info-text {
margin-right: 20rpx;
color: #999999;
font-size: 24rpx;
}
.media-foot {
margin-top: 20rpx;
flex-direction: row;
justify-content: space-between;
}
.max-close-view {
align-items: center;
justify-content: flex-end;
flex-direction: row;
height: 40rpx;
width: 80rpx;
}
.close-view {
border-style: solid;
border-width: 1px;
border-color: #999999;
border-radius: 10rpx;
justify-content: center;
height: 30rpx;
width: 40rpx;
line-height: 30rpx;
}
.close {
text-align: center;
color: #999999;
font-size: 28rpx;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
<template>
<text style="text-decoration:underline" :href="href" @click="openURL" :inWhiteList="inWhiteList">{{text}}</text>
</template>
<script>
/**
* @description u-link是一个外部网页超链接组件在小程序内打开内部web-view组件或复制url在app内打开外部浏览器在h5端打开新网页
* @property {String} href 点击后打开的外部网页url小程序中必须以https://开头
* @property {String} text 显示的文字
* @property {Boolean} inWhiteList 是否在小程序白名单中如果在的话在小程序端会直接打开内置web-view否则会只会复制url提示在外部打开
* @example * <u-link href="https://ext.dcloud.net.cn" text="https://ext.dcloud.net.cn" :inWhiteList="true"></u-link>
*/
export default {
name: 'u-link',
props: {
href: {
type: String,
default: ''
},
text: {
type: String,
default: ''
},
inWhiteList: {
type: Boolean,
default: false
}
},
methods: {
openURL() {
// #ifdef APP-PLUS
plus.runtime.openURL(this.href) //这里默认使用外部浏览器打开而不是内部web-view组件打开
// #endif
// #ifdef H5
window.open(this.href)
// #endif
// #ifdef MP
if (this.inWhiteList) { //如果在小程序的网址白名单中会走内置webview打开否则会复制网址提示在外部浏览器打开
uni.navigateTo({
url: '/pages/component/web-view/web-view?url=' + this.href
});
} else {
uni.setClipboardData({
data: this.href
});
uni.showModal({
content: '本网址无法直接在小程序内打开。已自动复制网址,请在手机浏览器里粘贴该网址',
showCancel: false
});
}
// #endif
}
}
}
</script>
<style>
</style>