初始化(包含登录模块)
This commit is contained in:
181
components/amap-wx/js/util.js
Normal file
181
components/amap-wx/js/util.js
Normal 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;
|
||||
1
components/amap-wx/lib/amap-wx.js
Normal file
1
components/amap-wx/lib/amap-wx.js
Normal file
File diff suppressed because one or more lines are too long
156
components/api-set-tabbar.nvue
Normal file
156
components/api-set-tabbar.nvue
Normal 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>
|
||||
1
components/marked/index.js
Normal file
1
components/marked/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export default './lib/marked'
|
||||
1573
components/marked/lib/marked.js
Normal file
1573
components/marked/lib/marked.js
Normal file
File diff suppressed because it is too large
Load Diff
55
components/mescroll-uni/components/mescroll-down.css
Normal file
55
components/mescroll-uni/components/mescroll-down.css
Normal 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);
|
||||
}
|
||||
}
|
||||
47
components/mescroll-uni/components/mescroll-down.vue
Normal file
47
components/mescroll-uni/components/mescroll-down.vue
Normal 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, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
|
||||
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>
|
||||
96
components/mescroll-uni/components/mescroll-empty.vue
Normal file
96
components/mescroll-uni/components/mescroll-empty.vue
Normal 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>
|
||||
83
components/mescroll-uni/components/mescroll-top.vue
Normal file
83
components/mescroll-uni/components/mescroll-top.vue
Normal 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>
|
||||
46
components/mescroll-uni/components/mescroll-up.css
Normal file
46
components/mescroll-uni/components/mescroll-up.css
Normal 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);
|
||||
}
|
||||
}
|
||||
39
components/mescroll-uni/components/mescroll-up.vue
Normal file
39
components/mescroll-uni/components/mescroll-up.vue
Normal 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 // 上拉加载的状态:0(loading前),1(loading中),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>
|
||||
14
components/mescroll-uni/mescroll-body.css
Normal file
14
components/mescroll-uni/mescroll-body.css
Normal 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);
|
||||
}
|
||||
}
|
||||
323
components/mescroll-uni/mescroll-body.vue
Normal file
323
components/mescroll-uni/mescroll-body.vue
Normal 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, // 下拉刷新状态 (inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
|
||||
upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),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>
|
||||
65
components/mescroll-uni/mescroll-mixins.js
Normal file
65
components/mescroll-uni/mescroll-mixins.js
Normal 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;
|
||||
34
components/mescroll-uni/mescroll-uni-option.js
Normal file
34
components/mescroll-uni/mescroll-uni-option.js
Normal 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
|
||||
32
components/mescroll-uni/mescroll-uni.css
Normal file
32
components/mescroll-uni/mescroll-uni.css
Normal 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);
|
||||
}
|
||||
}
|
||||
865
components/mescroll-uni/mescroll-uni.js
Normal file
865
components/mescroll-uni/mescroll-uni.js
Normal 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
|
||||
}
|
||||
396
components/mescroll-uni/mescroll-uni.vue
Normal file
396
components/mescroll-uni/mescroll-uni.vue
Normal 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>
|
||||
23
components/mescroll-uni/mixins/mescroll-comp.js
Normal file
23
components/mescroll-uni/mixins/mescroll-comp.js
Normal 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;
|
||||
51
components/mescroll-uni/mixins/mescroll-more-item.js
Normal file
51
components/mescroll-uni/mixins/mescroll-more-item.js
Normal 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;
|
||||
56
components/mescroll-uni/mixins/mescroll-more.js
Normal file
56
components/mescroll-uni/mixins/mescroll-more.js
Normal 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;
|
||||
12542
components/mpvue-citypicker/city-data/area.js
Normal file
12542
components/mpvue-citypicker/city-data/area.js
Normal file
File diff suppressed because it is too large
Load Diff
1503
components/mpvue-citypicker/city-data/city.js
Normal file
1503
components/mpvue-citypicker/city-data/city.js
Normal file
File diff suppressed because it is too large
Load Diff
139
components/mpvue-citypicker/city-data/province.js
Normal file
139
components/mpvue-citypicker/city-data/province.js
Normal 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;
|
||||
230
components/mpvue-citypicker/mpvueCityPicker.vue
Normal file
230
components/mpvue-citypicker/mpvueCityPicker.vue
Normal 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>
|
||||
123
components/mpvue-echarts/src/echarts.vue
Normal file
123
components/mpvue-echarts/src/echarts.vue
Normal 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>
|
||||
73
components/mpvue-echarts/src/wx-canvas.js
Normal file
73
components/mpvue-echarts/src/wx-canvas.js
Normal 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,
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
484
components/mpvue-picker/mpvuePicker.vue
Normal file
484
components/mpvue-picker/mpvuePicker.vue
Normal 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>
|
||||
175
components/mpvueGestureLock/gestureLock.js
Normal file
175
components/mpvueGestureLock/gestureLock.js
Normal 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;
|
||||
138
components/mpvueGestureLock/index.vue
Normal file
138
components/mpvueGestureLock/index.vue
Normal 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
362
components/music.vue
Normal 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>
|
||||
38
components/page-foot/page-foot.vue
Normal file
38
components/page-foot/page-foot.vue
Normal 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>
|
||||
16
components/page-head/page-head.vue
Normal file
16
components/page-head/page-head.vue
Normal 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
66
components/product.vue
Normal 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>
|
||||
21
components/public-module/public-module.vue
Normal file
21
components/public-module/public-module.vue
Normal 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>
|
||||
175
components/tab-nvue/mediaList.vue
Normal file
175
components/tab-nvue/mediaList.vue
Normal 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>
|
||||
5046
components/u-charts/u-charts.js
Normal file
5046
components/u-charts/u-charts.js
Normal file
File diff suppressed because it is too large
Load Diff
59
components/u-link/u-link.vue
Normal file
59
components/u-link/u-link.vue
Normal 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>
|
||||
Reference in New Issue
Block a user