1
This commit is contained in:
1
components/address.json
Normal file
1
components/address.json
Normal file
File diff suppressed because one or more lines are too long
116
components/applets-bind-userInfo/applets-bind-userInfo.vue
Normal file
116
components/applets-bind-userInfo/applets-bind-userInfo.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 绑定微信头像昵称弹窗 -->
|
||||
<view class="loginMask" v-if="bindUserInfoShow" @click="closePopup"></view>
|
||||
<view class="loginPopup" v-if="bindUserInfoShow">
|
||||
<view class="loginBox">
|
||||
<image class="logo" :src="base.logoUrl"></image>
|
||||
<view class="platformName">{{ base.platformName }}</view>
|
||||
<view class="description">需要获取用户头像和昵称</view>
|
||||
</view>
|
||||
<button type="primary" @click="onAuthorization">授权</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex';
|
||||
import base from '@/config/baseUrl';
|
||||
let clear;
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
base: base
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['userInfo', 'bindUserInfoShow'])
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['setUserInfo', 'setBindUserInfoShow']),
|
||||
//授权登录
|
||||
onAuthorization() {
|
||||
uni.getUserProfile({
|
||||
desc: '用于完善用户资料', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
|
||||
success: (res) => {
|
||||
this.setBindUserInfoShow(false);
|
||||
this.$http.post("api/mine/v1/sync_wx_info", {
|
||||
...res.userInfo
|
||||
}).then(res => {
|
||||
this.setUserInfo({
|
||||
nickname: res.userInfo.nickName,
|
||||
headImg: res.userInfo.avatarUrl,
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
closeLogin() {
|
||||
if (this.bindUserInfoShow && this.userInfo.token) {
|
||||
this.setBindUserInfoShow(false);
|
||||
}
|
||||
},
|
||||
//关闭弹窗
|
||||
closePopup() {
|
||||
this.setBindUserInfoShow(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/style/mixin.scss';
|
||||
.loginMask {
|
||||
position: fixed;
|
||||
top: 0upx;
|
||||
left: 0upx;
|
||||
right: 0upx;
|
||||
bottom: 0upx;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
z-index: 10;
|
||||
}
|
||||
.loginPopup {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
width: 500upx;
|
||||
background-color: #fff;
|
||||
border-radius: 20upx;
|
||||
overflow: hidden;
|
||||
z-index: 11;
|
||||
|
||||
.loginBox {
|
||||
padding: 30upx 15upx 40upx 15upx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
width: 160upx;
|
||||
height: 160upx;
|
||||
border-radius: 20%;
|
||||
}
|
||||
|
||||
.platformName {
|
||||
font-size: 24upx;
|
||||
color: #999;
|
||||
margin-top: 10upx;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 15upx;
|
||||
font-size: 30upx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0upx;
|
||||
background-color: $themeColor;
|
||||
}
|
||||
.active {
|
||||
background-color: $themeColor;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
71
components/battery/battery.vue
Normal file
71
components/battery/battery.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<view class="battery-container">
|
||||
<view class="battery-body">
|
||||
<view class="battery" :style="{width: `${level}%`}"></view>
|
||||
<text class="iconfont charging" v-if="charging"></text>
|
||||
</view>
|
||||
<view class="battery-head"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props:{
|
||||
level: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
charging: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.battery-container{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 25px;
|
||||
height: 10px;
|
||||
.battery-body{
|
||||
position: relative;
|
||||
padding: 1px;
|
||||
width: 22px;
|
||||
height: 100%;
|
||||
border-radius: 1px;
|
||||
border: $minor-text-color solid 1px;
|
||||
.battery{
|
||||
height: 100%;
|
||||
background-color: $minor-text-color;
|
||||
}
|
||||
.charging{
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 12px;
|
||||
line-height: 12px;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
.battery-head{
|
||||
width: 2px;
|
||||
height: 6px;
|
||||
background-color: $minor-text-color;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
186
components/common-logistics.vue
Normal file
186
components/common-logistics.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<view class="bg-white margin-top">
|
||||
<view class="common-logistics">
|
||||
<view class="logistic-item" v-for="(item,index) in logisticsData" :key="index">
|
||||
<view class="total-wrap" :style="{marginTop: item.isFirstNode ? '22rpx' : '6rpx'}">
|
||||
<view class="item-container">
|
||||
<view class="item-container-left flex flex-direction align-center"
|
||||
:class="[index == 0 ? 'text-1A1A1A' : 'text-808080']">
|
||||
<text class="text-df">{{item.monthDay}}</text>
|
||||
<text class="text-sm">{{item.hourMinute}}</text>
|
||||
</view>
|
||||
<view class="item-container-center">
|
||||
<view class="tag-container">
|
||||
<image v-if="item.isFirstNode && String(item.status) != 'null'" :src="nodeIconUrl(item.status,index)" mode="scaleToFill"></image>
|
||||
<view v-else class="item-tag-container">
|
||||
<image class="item-tag" :src="[index == 0 ? '/static/images/active-line-state.png' : '/static/images/line-state.png']" mode="scaleToFill"></image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="line-container"
|
||||
:style="{height: item.isFirstNode ? '145rpx' : '88rpx' , paddingTop: item.isFirstNode ? '22rpx': '8rpx'}">
|
||||
<view v-if="index !== logisticsData.length - 1" class="line" :style="{height: item.isFirstNode ? '120rpx':'80rpx'}"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-container-right" :style="{paddingTop: item.isFirstNode?'0':'8rpx'}">
|
||||
<view v-if="item.isFirstNode" class="item-title text-dm text-bold" :class="[index == 0 ? 'text-1A1A1A' : 'text-808080']">{{item.status}}</view>
|
||||
<view class="item-desc text-dm" :class="[index == 0 ? 'text-1A1A1A' : 'text-999999']" :style="{marginTop: item.isFirstNode ? '10rpx' : '0'}">{{item.context}}</view>
|
||||
<view class="item-time">{{item.createTime}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
logisticsData: {
|
||||
type: [Object, Array]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
nodeIconUrl() {
|
||||
return function(data, isFirstIndex) {
|
||||
// 0在途,1揽收,2疑难,3签收,4退签,5派件,6退回,7转单,10待清关,11清关中,12已清关,13清关异常,14收件人拒签
|
||||
// 图标根据需要自行更改,这里只作演示
|
||||
if (data == '在途') {
|
||||
return isFirstIndex === 0 ? '/static/images/logo.png' : '/static/images/logo.png'
|
||||
} else if (data == '揽收') {
|
||||
return isFirstIndex === 0 ? '/static/images/logo.png' : '/static/images/logo.png'
|
||||
} else if (data == '在途') {
|
||||
return isFirstIndex === 0 ? '/static/images/logo.png' : '/static/images/logo.png'
|
||||
} else if (data == '疑难') {
|
||||
return isFirstIndex === 0 ? '/static/images/logo.png' : '/static/images/logo.png'
|
||||
} else if (data == '签收') {
|
||||
return isFirstIndex === 0 ? '/static/images/logo.png' : '/static/images/logo.png'
|
||||
} else if (data == '退签') {
|
||||
return isFirstIndex === 0 ? '/static/images/logo.png' : '/static/images/logo.png'
|
||||
} else if (data == '派件') {
|
||||
return isFirstIndex === 0 ? '/static/images/logo.png' : '/static/images/logo.png'
|
||||
} else if (data == '退回') {
|
||||
return isFirstIndex === 0 ? '/static/images/logo.png' : '/static/images/logo.png'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.common-logistics {
|
||||
height: auto;
|
||||
box-sizing: border-box;
|
||||
padding: 20rpx 30rpx 50rpx;
|
||||
}
|
||||
|
||||
.item-container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
|
||||
.item-container-left {
|
||||
width: 120rpx;
|
||||
max-width: 120rpx;
|
||||
}
|
||||
|
||||
.item-container-center {
|
||||
width: 44rpx;
|
||||
height: auto;
|
||||
|
||||
.tag-container {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
|
||||
image {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.item-tag-container {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.item-tag {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line-container {
|
||||
box-sizing: border-box;
|
||||
width: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.line {
|
||||
width: 2rpx;
|
||||
background-color: #dcdcdc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-container-right {
|
||||
width: 510rpx;
|
||||
max-width: 510rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10rpx 0 24rpx;
|
||||
|
||||
.item-title {
|
||||
width: 100%;
|
||||
height: 40rpx;
|
||||
line-height: 44rpx;
|
||||
color: #222;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
margin-top: 16rpx;
|
||||
width: 100%;
|
||||
min-height: 30rpx;
|
||||
line-height: 30rpx;
|
||||
word-wrap: break-word;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.item-time {
|
||||
margin-top: 12rpx;
|
||||
width: 100%;
|
||||
height: 34rpx;
|
||||
line-height: 34rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line-state {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.take-space {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.text-1A1A1A {
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.text-999999 {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.text-808080 {
|
||||
color: #808080;
|
||||
}
|
||||
</style>
|
||||
332
components/complete-waterfall-goods/complete-waterfall-goods.vue
Normal file
332
components/complete-waterfall-goods/complete-waterfall-goods.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<template>
|
||||
<view class="flow-box" :style="'height: ' + loadingTop + 'rpx'" ref="flow_box">
|
||||
<view v-for="(item, index) in newList" :key="item.objId" class="item good_item" :class="positionList[item.objId].right ? 'right' : 'left'" v-show="positionList[item.objId].imageLoad" :style="{top: positionList[item.objId].top + 'rpx'}"
|
||||
@click="onPageJump(item)">
|
||||
<view class="goods_img">
|
||||
<image :data-id="item.objId" @load="onImageLoad" :src="item.img" mode="widthFix"></image>
|
||||
</view>
|
||||
<view class="title">
|
||||
<view v-for="(childItem,childIndex) of positionList[item.objId].titleList" :key="childIndex">{{childItem.content}}</view>
|
||||
</view>
|
||||
<view class="sell_well">
|
||||
<text>热销</text>
|
||||
</view>
|
||||
<view class="info">
|
||||
<text class="money"><text>{{ item.priceDiscount }}</text></text>
|
||||
<text class="count">{{ item.numSales || 0 }}人购买</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const defaultData = {
|
||||
// 起始距离(rpx)
|
||||
startTop: 20,
|
||||
// 除了图片和标题以外的高度(rpx)
|
||||
contentHeight: 40 + 32 + 20 + 30 + 60,
|
||||
// 瀑布流容器宽度(rpx)
|
||||
viewWidth: 344,
|
||||
// 标题可显示宽度(rpx)
|
||||
titleWidth: 300,
|
||||
// 标题文字大小(rpx)
|
||||
titleSize: 28,
|
||||
// 容器之间的间隔Y轴(rpx)
|
||||
viewSpace: 20,
|
||||
// 列表ID参数名称
|
||||
idName: "objId",
|
||||
// 列表标题参数名称
|
||||
titleName: "name",
|
||||
};
|
||||
|
||||
// 文字换行计算行数
|
||||
function drawtext(text, maxWidth) {
|
||||
let textArr = text.split("");
|
||||
let len = textArr.length;
|
||||
// 上个节点
|
||||
let previousNode = 0;
|
||||
// 记录节点宽度
|
||||
let nodeWidth = 0;
|
||||
// 文本换行数组
|
||||
let rowText = [];
|
||||
// 如果是字母,侧保存长度
|
||||
let letterWidth = 0;
|
||||
// 汉字宽度
|
||||
let chineseWidth = defaultData.titleSize + 2;
|
||||
// otherFont宽度
|
||||
let otherWidth = (defaultData.titleSize + 2) / 2;
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (/[\u4e00-\u9fa5]|[\uFE30-\uFFA0]/g.test(textArr[i])) {
|
||||
if(letterWidth > 0){
|
||||
if(nodeWidth + chineseWidth + letterWidth * otherWidth > maxWidth){
|
||||
rowText.push({
|
||||
type: "text",
|
||||
content: text.substring(previousNode, i)
|
||||
});
|
||||
previousNode = i;
|
||||
nodeWidth = chineseWidth;
|
||||
letterWidth = 0;
|
||||
} else {
|
||||
nodeWidth += chineseWidth + letterWidth * otherWidth;
|
||||
letterWidth = 0;
|
||||
}
|
||||
} else {
|
||||
if(nodeWidth + chineseWidth > maxWidth){
|
||||
rowText.push({
|
||||
type: "text",
|
||||
content: text.substring(previousNode, i)
|
||||
});
|
||||
previousNode = i;
|
||||
nodeWidth = chineseWidth;
|
||||
}else{
|
||||
nodeWidth += chineseWidth;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(/\n/g.test(textArr[i])){
|
||||
rowText.push({
|
||||
type: "break",
|
||||
content: text.substring(previousNode, i)
|
||||
});
|
||||
previousNode = i + 1;
|
||||
nodeWidth = 0;
|
||||
letterWidth = 0;
|
||||
}else if(textArr[i] == "\\" && textArr[i + 1] == "n"){
|
||||
rowText.push({
|
||||
type: "break",
|
||||
content: text.substring(previousNode, i)
|
||||
});
|
||||
previousNode = i + 2;
|
||||
nodeWidth = 0;
|
||||
letterWidth = 0;
|
||||
}else if(/[a-zA-Z0-9]/g.test(textArr[i])){
|
||||
letterWidth += 1;
|
||||
if(nodeWidth + letterWidth * otherWidth > maxWidth){
|
||||
rowText.push({
|
||||
type: "text",
|
||||
content: text.substring(previousNode, i + 1 - letterWidth)
|
||||
});
|
||||
previousNode = i + 1 - letterWidth;
|
||||
nodeWidth = letterWidth * otherWidth;
|
||||
letterWidth = 0;
|
||||
}
|
||||
} else{
|
||||
if(nodeWidth + otherWidth > maxWidth){
|
||||
rowText.push({
|
||||
type: "text",
|
||||
content: text.substring(previousNode, i)
|
||||
});
|
||||
previousNode = i;
|
||||
nodeWidth = otherWidth;
|
||||
}else{
|
||||
nodeWidth += otherWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (previousNode < len) {
|
||||
rowText.push({
|
||||
type: "text",
|
||||
content: text.substring(previousNode, len)
|
||||
});
|
||||
}
|
||||
return rowText;
|
||||
}
|
||||
let allow = true;
|
||||
export default {
|
||||
props: {
|
||||
// 数据列表
|
||||
list: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newList: [],
|
||||
positionList: {},
|
||||
loadingTop: 0,
|
||||
leftHistoryTop: 0,
|
||||
rightHistoryTop: 0
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// 数据
|
||||
list: function(newVal, oldVal) {
|
||||
this.newList = newVal;
|
||||
this.getCalculationPosition();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onPageJump(item) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/home/shop/goodsDetail?objId=" + item.objId
|
||||
})
|
||||
},
|
||||
refreshData() {},
|
||||
// 节流
|
||||
throttle(callback,time = 400){
|
||||
if(!allow) return false;
|
||||
allow = false;
|
||||
setTimeout(function(){
|
||||
allow = true;
|
||||
callback();
|
||||
},time);
|
||||
},
|
||||
// 获取数据位置信息 top,right
|
||||
getCalculationPosition(){
|
||||
let leftHistoryTop = defaultData.startTop;
|
||||
let rightHistoryTop = defaultData.startTop;
|
||||
let positionList = this.positionList;
|
||||
this.newList.forEach((item,index) => {
|
||||
let currentHeight = defaultData.contentHeight;
|
||||
let positionItem = {};
|
||||
// 查看是否有位置数据
|
||||
if(positionList[item[defaultData.idName]]){
|
||||
positionItem = positionList[item[defaultData.idName]];
|
||||
}else {
|
||||
positionList[item[defaultData.idName]] = {};
|
||||
}
|
||||
// 查看是否有图片高度数据,没有默认等宽
|
||||
if(positionItem.imageHeight){
|
||||
currentHeight += positionItem.imageHeight;
|
||||
positionList[item[defaultData.idName]].imageLoad = true;
|
||||
}else{
|
||||
currentHeight += defaultData.viewWidth;
|
||||
}
|
||||
// 查看是否有标题高度数据,没有获取高度
|
||||
if(positionItem.titleHeight){
|
||||
currentHeight += positionItem.titleHeight;
|
||||
}else{
|
||||
let titleList = drawtext(item[defaultData.titleName], defaultData.titleWidth);
|
||||
let titleListLen = titleList.length;
|
||||
positionList[item[defaultData.idName]].titleList = titleList;
|
||||
positionList[item[defaultData.idName]].titleHeight = titleListLen * (defaultData.titleSize + 6);
|
||||
currentHeight += titleListLen * (defaultData.titleSize + 6);
|
||||
}
|
||||
if(leftHistoryTop > rightHistoryTop){
|
||||
positionList[item[defaultData.idName]].top = rightHistoryTop;
|
||||
positionList[item[defaultData.idName]].right = true;
|
||||
positionList[item[defaultData.idName]].height = currentHeight;
|
||||
rightHistoryTop += currentHeight + defaultData.viewSpace;
|
||||
}else{
|
||||
positionList[item[defaultData.idName]].top = leftHistoryTop;
|
||||
positionList[item[defaultData.idName]].right = false;
|
||||
positionList[item[defaultData.idName]].height = currentHeight;
|
||||
leftHistoryTop += currentHeight + defaultData.viewSpace;
|
||||
}
|
||||
});
|
||||
if(leftHistoryTop > rightHistoryTop){
|
||||
this.loadingTop = leftHistoryTop;
|
||||
}else{
|
||||
this.loadingTop = rightHistoryTop;
|
||||
}
|
||||
this.positionList = positionList;
|
||||
this.$forceUpdate();
|
||||
},
|
||||
// 图片加载完成
|
||||
onImageLoad(e){
|
||||
let id = e.currentTarget.dataset.id;
|
||||
let scale = defaultData.viewWidth / e.detail.width;
|
||||
let height = scale * e.detail.height;
|
||||
if(this.positionList[id]){
|
||||
this.positionList[id].imageHeight = height;
|
||||
}else{
|
||||
this.positionList[id] = {
|
||||
imageHeight: height
|
||||
}
|
||||
}
|
||||
// 图片加载完刷新位置节流一下,提升性能
|
||||
this.throttle(() => {
|
||||
this.getCalculationPosition();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/style/mixin.scss';
|
||||
|
||||
.flow-box {
|
||||
position: relative;
|
||||
color: #1a1a1a;
|
||||
padding: 0 20rpx 0rpx 20rpx;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.flow-box .left {
|
||||
left: 20rpx;
|
||||
}
|
||||
|
||||
.flow-box .right {
|
||||
right: 20rpx;
|
||||
}
|
||||
|
||||
.flow-box .good_item {
|
||||
position: absolute;
|
||||
width: 345rpx;
|
||||
border: 2rpx solid #f9f9f9;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&.noMargin {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.goods_img {
|
||||
image {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 20rpx 20rpx;
|
||||
color: #333333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.sell_well {
|
||||
display: flex;
|
||||
padding: 0rpx 20rpx 20rpx 20rpx;
|
||||
|
||||
text {
|
||||
height: 32rpx;
|
||||
border-radius: 4rpx;
|
||||
border: solid 2rpx $themeColor;
|
||||
line-height: 28rpx;
|
||||
padding: 0 15rpx;
|
||||
font-size: 24rpx;
|
||||
color: $themeColor;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 100%;
|
||||
padding: 0rpx 20rpx 30rpx 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.money {
|
||||
font-size: 26rpx;
|
||||
color: #ff3d3d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
text {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
506
components/date-popup/date-popup.vue
Normal file
506
components/date-popup/date-popup.vue
Normal file
@@ -0,0 +1,506 @@
|
||||
<template>
|
||||
<picker mode="multiSelector" @change="dateChange" @columnchange="dateColumnChange" :value="dateIndex" :range="dateList" range-key="name"><slot></slot></picker>
|
||||
</template>
|
||||
<script>
|
||||
let dateColumn = 0;
|
||||
let hoursColumn = 0;
|
||||
export default {
|
||||
props: {
|
||||
//可选天数(不能超过50天)
|
||||
optionalDate:{
|
||||
type: Number,
|
||||
default() {
|
||||
return 2;
|
||||
}
|
||||
},
|
||||
// 最快多少分钟后
|
||||
bufferMinutes: {
|
||||
type: Number,
|
||||
default() {
|
||||
return 5;
|
||||
}
|
||||
},
|
||||
// 每天开始时间
|
||||
dayStartTime:{
|
||||
type: String,
|
||||
default() {
|
||||
return '00:00';
|
||||
}
|
||||
},
|
||||
// 每天结束时间
|
||||
dayEndTime:{
|
||||
type: String,
|
||||
default() {
|
||||
return '24:00';
|
||||
}
|
||||
},
|
||||
// 是否显示立即配送
|
||||
shipNow:{
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// 开始时间
|
||||
startTime:{
|
||||
type: String,
|
||||
default() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dateList: [[], [], []],
|
||||
dateIndex: [0, 0, 0],
|
||||
// 是否当天
|
||||
isThatDay: true
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.initDate();
|
||||
},
|
||||
watch: {
|
||||
startTime(val){
|
||||
this.initDate();
|
||||
}
|
||||
},
|
||||
//方法
|
||||
methods: {
|
||||
//配送时间
|
||||
dateChange(e) {
|
||||
let value = e.detail.value;
|
||||
this.dateIndex = value;
|
||||
let hours = this.dateList[1][value[1]];
|
||||
let minutes = this.dateList[2][value[2]];
|
||||
let timeName = '';
|
||||
if (value[0] > 0) {
|
||||
timeName = hours.name + minutes.name;
|
||||
}
|
||||
this.$emit('change', {
|
||||
dateName: this.dateList[0][value[0]].name,
|
||||
dateValue: this.dateList[0][value[0]].value,
|
||||
timeName: timeName,
|
||||
timeValue: (hours.value < 10 ? '0' + hours.value : hours.value) + ':' + (minutes.value < 10 ? '0' + minutes.value : minutes.value)
|
||||
});
|
||||
},
|
||||
//配送时间选择时
|
||||
dateColumnChange(e) {
|
||||
let today = 0;
|
||||
if(this.shipNow){
|
||||
today = 1;
|
||||
}
|
||||
let date = void 0;
|
||||
if(this.startTime){
|
||||
let startTimeStr = this.startTime.replace(/-/g,"/");
|
||||
if(!/\s/g.test(startTimeStr)){
|
||||
startTimeStr += " 00:00:00"
|
||||
}
|
||||
date = new Date(startTimeStr);
|
||||
} else {
|
||||
date = new Date();
|
||||
}
|
||||
//当前时间
|
||||
let startTimeObj = {
|
||||
year: date.getFullYear(),
|
||||
month: date.getMonth() + 1,
|
||||
date: date.getDate(),
|
||||
hours: date.getHours(),
|
||||
minutes: date.getMinutes()
|
||||
};
|
||||
//当前时间**分钟后的日期
|
||||
let currentStartTime = this.getRecentTime(startTimeObj, {
|
||||
type: 'minutes',
|
||||
num: this.bufferMinutes
|
||||
});
|
||||
//判断用户滑动的第一例 日期
|
||||
if (e.detail.column == 0) {
|
||||
//每次滑动第一列将储存index
|
||||
dateColumn = e.detail.value;
|
||||
//滑动到第一列第一个还原数据
|
||||
if (e.detail.value == 0 && this.shipNow) {
|
||||
this.dateList[1] = [
|
||||
{
|
||||
name: '-',
|
||||
value: currentStartTime.hours
|
||||
}
|
||||
];
|
||||
this.dateList[2] = [
|
||||
{
|
||||
name: '-',
|
||||
value: currentStartTime.minutes
|
||||
}
|
||||
];
|
||||
} else {
|
||||
if (e.detail.value == today && this.isThatDay) {
|
||||
this.dateList[1] = this.getHoursList(currentStartTime.hours);
|
||||
this.dateList[2] = this.getMinutesList(currentStartTime.hours, currentStartTime.minutes);
|
||||
} else {
|
||||
this.dateList[1] = this.getHoursList();
|
||||
this.dateList[2] = this.getMinutesList(this.dateList[1][hoursColumn].value);
|
||||
}
|
||||
}
|
||||
} else if (e.detail.column == 1) { //判断用户滑动的第二例 小时
|
||||
hoursColumn = e.detail.value;
|
||||
if (dateColumn == today && e.detail.value == 0 && this.isThatDay) {
|
||||
if(this.dateList[1][0].value < currentStartTime.hours){
|
||||
this.dateList[1].splice(0,1);
|
||||
}
|
||||
this.dateList[2] = this.getMinutesList(currentStartTime.hours, currentStartTime.minutes);
|
||||
}else{
|
||||
this.dateList[2] = this.getMinutesList(this.dateList[1][e.detail.value].value);
|
||||
}
|
||||
}
|
||||
this.$forceUpdate();
|
||||
},
|
||||
//获取月份对应的天数
|
||||
getDateMax(year, month) {
|
||||
if (month == 2) {
|
||||
return year % 4 == 0 ? 29 : 28;
|
||||
} else if ([1,3,5,7,8,10,12].includes(month)) {
|
||||
return 31;
|
||||
} else {
|
||||
return 30;
|
||||
}
|
||||
},
|
||||
//获取日期相加后的结果
|
||||
dateVal(dateInfo, addMonth = 0) {
|
||||
dateInfo.month = dateInfo.month;
|
||||
if (dateInfo.month > 12) {
|
||||
dateInfo.month = 1;
|
||||
dateInfo.year += 1;
|
||||
}
|
||||
let dateMax = this.getDateMax(dateInfo.year, dateInfo.month);
|
||||
if (dateInfo.date > dateMax) {
|
||||
dateInfo.date = dateInfo.date - dateMax;
|
||||
dateInfo.month += 1;
|
||||
addMonth += 1;
|
||||
return this.dateVal(dateInfo, addMonth);
|
||||
} else {
|
||||
return {
|
||||
...dateInfo,
|
||||
addMonth: addMonth
|
||||
};
|
||||
}
|
||||
},
|
||||
//获取年月日时分相加后的日期
|
||||
getRecentTime(data, add) {
|
||||
const _this = this;
|
||||
//时间相加
|
||||
function dateCalculation(res) {
|
||||
if (res.type == 'minutes') {
|
||||
if (data.minutes + res.num >= 55) {
|
||||
if(data.minutes + res.num <= 60){
|
||||
dateCalculation({
|
||||
type: 'hours',
|
||||
num: 1
|
||||
});
|
||||
data.minutes = 0;
|
||||
} else {
|
||||
dateCalculation({
|
||||
type: 'hours',
|
||||
num: parseInt((data.minutes + res.num) / 60)
|
||||
});
|
||||
data.minutes = (data.minutes + res.num) % 60;
|
||||
}
|
||||
|
||||
} else {
|
||||
data.minutes = data.minutes + res.num;
|
||||
}
|
||||
} else if (res.type == 'hours') {
|
||||
if (data.hours + res.num >= 24) {
|
||||
dateCalculation({
|
||||
type: 'date',
|
||||
num: parseInt((data.hours + res.num) / 24)
|
||||
});
|
||||
data.hours = (data.hours + res.num) % 24;
|
||||
} else {
|
||||
data.hours = data.hours + res.num;
|
||||
}
|
||||
} else if (res.type == 'date') {
|
||||
let value = _this.dateVal({
|
||||
date: data.date + res.num,
|
||||
month: data.month,
|
||||
year: data.year
|
||||
});
|
||||
if (value.addMonth > 0) {
|
||||
dateCalculation({
|
||||
type: 'month',
|
||||
num: value.addMonth
|
||||
});
|
||||
}
|
||||
data.date = value.date;
|
||||
} else if (res.type == 'month') {
|
||||
if (data.month + res.num > 12) {
|
||||
dateCalculation({
|
||||
type: 'year',
|
||||
num: parseInt((data.month + res.num) / 24)
|
||||
});
|
||||
data.month = (data.month + res.num) % 12;
|
||||
} else {
|
||||
data.hours = data.hours + res.num;
|
||||
}
|
||||
} else if (res.type == 'year') {
|
||||
data.year += res.num;
|
||||
}
|
||||
}
|
||||
dateCalculation(add);
|
||||
return data;
|
||||
},
|
||||
//初始化时间
|
||||
initDate() {
|
||||
let date = new Date();
|
||||
//当前时间
|
||||
let currentTime = {
|
||||
year: date.getFullYear(),
|
||||
month: date.getMonth() + 1,
|
||||
date: date.getDate(),
|
||||
hours: date.getHours(),
|
||||
minutes: date.getMinutes()
|
||||
};
|
||||
let selectTime = void 0;
|
||||
if(this.startTime){
|
||||
let startDateStr = this.startTime.replace(/-/g,"/");
|
||||
if(!/\s/g.test(startDateStr)){
|
||||
startDateStr += " 00:00:00";
|
||||
}
|
||||
let startDate = new Date(startDateStr);
|
||||
selectTime = {
|
||||
year: startDate.getFullYear(),
|
||||
month: startDate.getMonth() + 1,
|
||||
date: startDate.getDate(),
|
||||
hours: startDate.getHours(),
|
||||
minutes: startDate.getMinutes()
|
||||
};
|
||||
if(currentTime.year == selectTime.year && currentTime.month == selectTime.month && currentTime.date == selectTime.date){
|
||||
this.isThatDay = true;
|
||||
} else {
|
||||
this.isThatDay = false;
|
||||
}
|
||||
} else {
|
||||
selectTime = currentTime;
|
||||
}
|
||||
//当前时间**分钟后的日期
|
||||
let startTimeObj = this.getRecentTime(selectTime, {
|
||||
type: 'minutes',
|
||||
num: this.bufferMinutes
|
||||
});
|
||||
//设置日期选择器默认数据
|
||||
this.getDefaultList(startTimeObj, currentTime);
|
||||
let changeData = {};
|
||||
if(this.dateList.length >= 3){
|
||||
if(this.dateList[0].length > 0){
|
||||
let dateData = this.dateList[0][0];
|
||||
changeData.dateName = dateData.name;
|
||||
changeData.dateValue = dateData.value;
|
||||
}
|
||||
if(this.dateList[1].length > 0){
|
||||
let timeData = this.dateList[1][0];
|
||||
changeData.timeName = timeData.name;
|
||||
changeData.timeValue = timeData.value < 10 ? '0' + timeData.value : timeData.value;
|
||||
}
|
||||
if(this.dateList[2].length > 0){
|
||||
let timeData = this.dateList[2][0];
|
||||
changeData.timeName += timeData.name;
|
||||
changeData.timeValue += ":" + (timeData.value < 10 ? '0' + timeData.value : timeData.value);
|
||||
}
|
||||
}
|
||||
this.$emit('change', changeData);
|
||||
},
|
||||
//获取默认数据
|
||||
getDefaultList(date, currentDate) {
|
||||
let dateList = [];
|
||||
if(this.shipNow && !this.startTime){
|
||||
const firstName = '立即配送';
|
||||
const firstValue = date.year + '-' + (date.month < 10 ? '0' + date.month : date.month) + '-' + (date.date < 10 ? '0' + date.date : date.date);
|
||||
dateList.push({
|
||||
name: firstName,
|
||||
value: firstValue,
|
||||
type: "now"
|
||||
});
|
||||
this.dateList[1].push({
|
||||
name: '-',
|
||||
value: date.date,
|
||||
});
|
||||
this.dateList[2].push({
|
||||
name: '-',
|
||||
value: date.minutes,
|
||||
});
|
||||
//组件返回初始值
|
||||
this.$emit('change', {
|
||||
dateName: firstName,
|
||||
dateValue: firstValue,
|
||||
timeName: '',
|
||||
timeValue: (date.hours < 10 ? '0' + date.hours : date.hours) + ':' + (date.minutes < 10 ? '0' + date.minutes : date.minutes)
|
||||
});
|
||||
}
|
||||
//获取日期数据表并和立即维修合并
|
||||
this.dateList[0] = dateList.concat(this.getDateList(date, currentDate));
|
||||
if(!this.shipNow){
|
||||
if(this.isThatDay){
|
||||
this.dateList[1] = this.getHoursList(date.hours);
|
||||
this.dateList[2] = this.getMinutesList(date.hours, date.minutes);
|
||||
} else {
|
||||
this.dateList[1] = this.getHoursList();
|
||||
this.dateList[2] = this.getMinutesList(this.dateList[1][0].value);
|
||||
}
|
||||
}
|
||||
},
|
||||
//获取日期排序
|
||||
getDateList(date, currentDate) {
|
||||
let dateList = [];
|
||||
let monthMax = date.date + this.optionalDate;
|
||||
let currentMonthMax = this.getDateMax(date.year, date.month);
|
||||
let tomorrowDateValue = this.dateVal({
|
||||
year: currentDate.year,
|
||||
month: currentDate.month,
|
||||
date: currentDate.date + 1
|
||||
});
|
||||
//遍历日期选择
|
||||
for (let i = date.date; i < monthMax; i++) {
|
||||
let dateStr, name;
|
||||
if (i <= currentMonthMax && this.isThatDay) {
|
||||
dateStr = date.year + '-' + (date.month < 10 ? '0' + date.month : date.month) + '-' + (i < 10 ? '0' + i : i);
|
||||
if (currentDate.date == i) {
|
||||
name = '今天';
|
||||
} else if (currentDate.date + 1 == i) {
|
||||
name = '明天';
|
||||
} else {
|
||||
name = currentDate.month + '月' + i + '日';
|
||||
}
|
||||
} else {
|
||||
let dateValue = this.dateVal({
|
||||
year: date.year,
|
||||
month: date.month,
|
||||
date: i
|
||||
});
|
||||
|
||||
dateStr =
|
||||
dateValue.year +
|
||||
'-' +
|
||||
(dateValue.month < 10 ? '0' + dateValue.month : dateValue.month) +
|
||||
'-' +
|
||||
(dateValue.date < 10 ? '0' + dateValue.date : dateValue.date);
|
||||
if (currentDate.month == dateValue.month && currentDate.date == dateValue.date) {
|
||||
name = '今天';
|
||||
} else if (tomorrowDateValue.month == dateValue.month && tomorrowDateValue.date == dateValue.date) {
|
||||
name = '明天';
|
||||
} else {
|
||||
name = dateValue.month + '月' + dateValue.date + '日';
|
||||
}
|
||||
}
|
||||
dateList.push({
|
||||
name: name,
|
||||
value: dateStr
|
||||
});
|
||||
}
|
||||
return dateList;
|
||||
},
|
||||
//获取小时排序
|
||||
getHoursList(minHours) {
|
||||
let hoursList = [];
|
||||
let startHours = 0;
|
||||
let endHours = 24;
|
||||
try {
|
||||
if (this.dayStartTime) {
|
||||
let arr = this.dayStartTime.split(':');
|
||||
startHours = parseInt(arr[0]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('开始时间转换失败,参数值:' + this.dayStartTime);
|
||||
}
|
||||
try {
|
||||
if (this.dayEndTime) {
|
||||
let arr = this.dayEndTime.split(':');
|
||||
if(parseInt(arr[1]) > 0){
|
||||
endHours = parseInt(arr[0]) + 1;
|
||||
}else{
|
||||
endHours = parseInt(arr[0]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('结束时间转换失败,参数值:' + this.dayEndTime);
|
||||
}
|
||||
if (minHours && minHours > startHours) {
|
||||
startHours = parseInt(minHours);
|
||||
}
|
||||
//遍历出小时选择值
|
||||
for (var h = startHours; h < endHours; h++) {
|
||||
if (h < 10) {
|
||||
hoursList.push({
|
||||
name: '0' + h + '时',
|
||||
value: h
|
||||
});
|
||||
} else {
|
||||
hoursList.push({
|
||||
name: h + '时',
|
||||
value: h
|
||||
});
|
||||
}
|
||||
}
|
||||
return hoursList;
|
||||
},
|
||||
//获取分钟排序
|
||||
getMinutesList(hours, minMinutes) {
|
||||
let minutesList = [];
|
||||
let startMinutes = 0;
|
||||
let endMinutes = 60;
|
||||
try {
|
||||
if (this.dayStartTime) {
|
||||
let arr = this.dayStartTime.split(':');
|
||||
if (parseInt(arr[0]) == hours) {
|
||||
startMinutes = parseInt(arr[1]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('开始时间转换失败,参数值:' + this.dayStartTime);
|
||||
}
|
||||
try {
|
||||
if (this.dayEndTime) {
|
||||
let arr = this.dayEndTime.split(':');
|
||||
if (parseInt(arr[0]) == hours) {
|
||||
endMinutes = parseInt(arr[1]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('结束时间转换失败,参数值:' + this.dayEndTime);
|
||||
}
|
||||
if (minMinutes && minMinutes > startMinutes) {
|
||||
startMinutes = parseInt(minMinutes);
|
||||
}
|
||||
if(startMinutes !== 0){
|
||||
let startBit;
|
||||
var minutesString = startMinutes.toString();
|
||||
//获取当前分钟第一位字符
|
||||
if (startMinutes < 10) {
|
||||
startBit = 0;
|
||||
} else {
|
||||
startBit = minutesString.charAt(0);
|
||||
}
|
||||
//获取当前分钟最后一位字符
|
||||
var lastBit = minutesString.charAt(minutesString.length - 1);
|
||||
if (lastBit > 5) {
|
||||
startMinutes = parseInt(parseInt(startBit) + 1 + '0');
|
||||
} else {
|
||||
startMinutes = parseInt(startBit + '5');
|
||||
}
|
||||
}
|
||||
//遍历出分钟选择值
|
||||
for (var m = startMinutes; m < endMinutes; m = m + 5) {
|
||||
if (m < 10) {
|
||||
minutesList.push({
|
||||
name: '0' + m + '分',
|
||||
value: m
|
||||
});
|
||||
} else {
|
||||
minutesList.push({
|
||||
name: m + '分',
|
||||
value: m
|
||||
});
|
||||
}
|
||||
}
|
||||
return minutesList;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
446
components/direction-swiper/direction-swiper.vue
Normal file
446
components/direction-swiper/direction-swiper.vue
Normal file
@@ -0,0 +1,446 @@
|
||||
<template>
|
||||
<view class="direction_swiper" ref="directionSwiper" @touchstart="onSwiperTouchstart" @touchmove="onSwiperTouchmove" @touchcancel="onSwiperTouchcancel" @touchend="onSwiperTouchend">
|
||||
<view class="swiper_content_box" ref="swiperContent" :style="{width: (screenWidth * tabData.length) + 'px', height: screenHeight + 'px' ,transform: 'translateX(' + translateX + 'px)', transition: 'transform ' + animationTime + 'ms ease'}">
|
||||
<view class="swiper_container" v-for="(item,index) of tabData" :key="index" :ref="'swiperContainer' + item.key" :style="{ width: screenWidth + 'px', height: (screenHeight * (item.list.length || 1)) + 'px', transform: 'translateY(' + swiperData[index].translateY + 'px)', transition: 'transform ' + animationTime + 'ms ease'}">
|
||||
<view v-if="item.list && item.list.length > 0" :style="{ width: screenWidth + 'px', height: (screenHeight * (item.list.length || 1)) + 'px'}">
|
||||
<view v-for="(childItem,childIndex) of item.list" :key="childIndex" class="swiper_item " v-if="Math.abs(swiperData[swiperIndex].swiperItemIndex - childIndex) < 3" :style="{ top: (childIndex * screenHeight) + 'px', width: screenWidth + 'px', height: screenHeight + 'px' }">
|
||||
<slot :item="item" :childItem="childItem" :index="index" :childIndex="childIndex"></slot>
|
||||
</view>
|
||||
</view>
|
||||
<view class="swiper_empty" v-else :style="{ width: screenWidth + 'px', height: screenHeight + 'px' }">
|
||||
<image class="swiper_empty_image" src="http://qn.kemean.cn/upload/202104/12/1618198729835lgteuvym.png" mode="aspectFit"></image>
|
||||
<text class="swiper_empty_text">{{ item.emptyText || '暂无数据' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
let clearTime = null;
|
||||
let lastTime = 0;
|
||||
// #ifdef APP-NVUE
|
||||
const animation = weex.requireModule('animation');
|
||||
const dom = weex.requireModule('dom');
|
||||
// #endif
|
||||
export default {
|
||||
props: {
|
||||
list: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
current: {
|
||||
type: Number,
|
||||
default () {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
//开始触摸时间
|
||||
startTime: 0,
|
||||
//开始触摸距离
|
||||
touchStartY: 0,
|
||||
touchStartX: 0,
|
||||
//上次的位置
|
||||
currentY: 0,
|
||||
currentX: 0,
|
||||
contentTranslateX: 0,
|
||||
tabData: [],
|
||||
swiperData: [],
|
||||
animationTime: 0,
|
||||
animationDirection: "Y",
|
||||
screenHeight: 0,
|
||||
screenWidth: 0,
|
||||
// 是否允许滑动
|
||||
canSlide: true,
|
||||
// 手指数量
|
||||
fingersNumber: 0,
|
||||
swiperIndex: 0,
|
||||
translateX: 0,
|
||||
};
|
||||
},
|
||||
//第一次加载
|
||||
created() {
|
||||
let systemInfo = uni.getSystemInfoSync();
|
||||
this.screenWidth = systemInfo.screenWidth;
|
||||
this.screenHeight = systemInfo.screenHeight;
|
||||
this.$nextTick(() => {
|
||||
this.getContainerWidthHeight();
|
||||
});
|
||||
let swiperData = [];
|
||||
if(this.list && this.list.length > 0){
|
||||
this.list.forEach(item => {
|
||||
if(item.key && item.list){
|
||||
let objectData = Object.assign({
|
||||
list: [],
|
||||
}, item);
|
||||
this.tabData.push(objectData);
|
||||
swiperData.push({
|
||||
//滑动距离
|
||||
translateY: 0,
|
||||
swiperItemIndex: 0,
|
||||
length: item.list.length || 0,
|
||||
key: item.key
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: "参数没有key或list",
|
||||
icon: "none"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
this.swiperData = swiperData;
|
||||
},
|
||||
watch: {
|
||||
list(val){
|
||||
let tabData = [];
|
||||
if(val && val.length > 0){
|
||||
val.forEach((item,index) => {
|
||||
if(item.key){
|
||||
tabData.push(item);
|
||||
this.swiperData[index].length = item.list.length || 0;
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: "【list】参数没有key",
|
||||
icon: "none"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
this.tabData = tabData;
|
||||
this.$forceUpdate();
|
||||
},
|
||||
current(val){
|
||||
this.onType(val);
|
||||
}
|
||||
},
|
||||
//方法
|
||||
methods: {
|
||||
// 获取内容的高度
|
||||
getContainerWidthHeight(){
|
||||
// #ifdef APP-NVUE
|
||||
dom.getComponentRect(this.$refs['directionSwiper'], (data) => {
|
||||
this.screenWidth = data.size.width;
|
||||
this.screenHeight = data.size.height;
|
||||
});
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
uni.createSelectorQuery().in(this).selectAll('.direction_swiper')
|
||||
.boundingClientRect(data => {
|
||||
this.screenWidth = data[0].width;
|
||||
this.screenHeight = data[0].height;
|
||||
}).exec()
|
||||
// #endif
|
||||
},
|
||||
onType(index){
|
||||
this.swiperIndex = index;
|
||||
this.onChangeY();
|
||||
this.contentTranslateX = -(index * this.screenWidth);
|
||||
// #ifdef APP-NVUE
|
||||
let swiperContent = this.$refs.swiperContent;
|
||||
if(swiperContent){
|
||||
if(Array.isArray(swiperContent)){
|
||||
swiperContent = swiperContent[0];
|
||||
}
|
||||
animation.transition(this.$refs.swiperContent, {
|
||||
styles: {
|
||||
transform: 'translate('+ this.contentTranslateX +'px, 0px)',
|
||||
transformOrigin: 'center center'
|
||||
},
|
||||
duration: 360, //ms
|
||||
timingFunction: 'ease',
|
||||
delay: 0 //ms
|
||||
}, function () {
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
this.translateX = this.contentTranslateX;
|
||||
this.animationTime = 360;
|
||||
// #endif
|
||||
},
|
||||
onChangeY() {
|
||||
this.$emit("changeY", {
|
||||
...this.tabData[this.swiperIndex],
|
||||
...this.swiperData[this.swiperIndex],
|
||||
});
|
||||
},
|
||||
setAnimationY(translateY, animationTime, indexChange){
|
||||
this.animationTime = animationTime;
|
||||
let swiperItem = this.swiperData[this.swiperIndex];
|
||||
swiperItem.translateY = translateY;
|
||||
if(indexChange){
|
||||
if(indexChange == "less" && swiperItem.swiperItemIndex > 0){
|
||||
swiperItem.swiperItemIndex -= 1;
|
||||
} else if(indexChange == "plus" && swiperItem.swiperItemIndex < swiperItem.length - 1){
|
||||
swiperItem.swiperItemIndex += 1;
|
||||
}
|
||||
}
|
||||
this.$set(this.swiperData, this.swiperIndex, swiperItem);
|
||||
// #ifdef APP-NVUE
|
||||
let swiperContainer = this.$refs['swiperContainer' + swiperItem.key];
|
||||
if(swiperContainer){
|
||||
if(Array.isArray(swiperContainer)){
|
||||
swiperContainer = swiperContainer[0];
|
||||
}
|
||||
animation.transition(swiperContainer, {
|
||||
styles: {
|
||||
transform: 'translate(0px, ' + translateY + 'px)',
|
||||
transformOrigin: 'center center'
|
||||
},
|
||||
duration: animationTime, //ms
|
||||
timingFunction: 'ease',
|
||||
delay: 0 //ms
|
||||
}, function () { });
|
||||
}
|
||||
// #endif
|
||||
setTimeout(() => {
|
||||
this.canSlide = true;
|
||||
}, animationTime);
|
||||
},
|
||||
setAnimationX(translateX, animationTime, indexChange){
|
||||
if(indexChange){
|
||||
if(indexChange == "less" && this.swiperIndex > 0){
|
||||
this.swiperIndex -= 1;
|
||||
this.contentTranslateX = translateX;
|
||||
this.$emit("changeX", {
|
||||
swiperIndex: this.swiperIndex,
|
||||
...this.swiperData[this.swiperIndex]
|
||||
});
|
||||
this.onChangeY();
|
||||
} else if(indexChange == "plus" && this.swiperIndex < this.swiperData.length - 1){
|
||||
this.swiperIndex += 1;
|
||||
this.contentTranslateX = translateX;
|
||||
this.$emit("changeX", {
|
||||
swiperIndex: this.swiperIndex,
|
||||
...this.swiperData[this.swiperIndex]
|
||||
});
|
||||
this.onChangeY();
|
||||
}
|
||||
}
|
||||
// #ifdef APP-NVUE
|
||||
let swiperContent = this.$refs.swiperContent;
|
||||
if(swiperContent){
|
||||
if(Array.isArray(swiperContent)){
|
||||
swiperContent = swiperContent[0];
|
||||
}
|
||||
animation.transition(swiperContent, {
|
||||
styles: {
|
||||
transform: 'translate('+ translateX +'px, 0px)',
|
||||
transformOrigin: 'center center'
|
||||
},
|
||||
duration: animationTime, //ms
|
||||
timingFunction: 'ease',
|
||||
delay: 0 //ms
|
||||
}, function () {
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
this.translateX = translateX;
|
||||
this.animationTime = animationTime;
|
||||
// #endif
|
||||
setTimeout(() => {
|
||||
this.canSlide = true;
|
||||
},animationTime);
|
||||
},
|
||||
// 手指触摸动作开始
|
||||
onSwiperTouchstart(e) {
|
||||
this.fingersNumber += 1;
|
||||
if(this.canSlide && this.fingersNumber == 1){
|
||||
//储存手指触摸坐标,当前时间戳,当前坐标
|
||||
// #ifdef APP-NVUE
|
||||
this.touchStartY = e.changedTouches[0].screenY;
|
||||
this.touchStartX = e.changedTouches[0].screenX;
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
this.touchStartY = e.changedTouches[0].clientY;
|
||||
this.touchStartX = e.changedTouches[0].clientX;
|
||||
// #endif
|
||||
let startTime = new Date().getTime();
|
||||
this.startTime = startTime;
|
||||
lastTime = startTime;
|
||||
if(this.swiperData[this.swiperIndex]){
|
||||
this.currentY = this.swiperData[this.swiperIndex].translateY;
|
||||
} else {
|
||||
this.currentY = 0;
|
||||
}
|
||||
this.currentX = this.contentTranslateX;
|
||||
this.animationDirection = "";
|
||||
}
|
||||
},
|
||||
// 手指触摸后移动
|
||||
onSwiperTouchmove(e) {
|
||||
if(this.canSlide && this.fingersNumber == 1){
|
||||
//手指当前坐标
|
||||
// #ifdef APP-NVUE
|
||||
const clientY = e.changedTouches[0].screenY;
|
||||
const clientX = e.changedTouches[0].screenX;
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
const clientY = e.changedTouches[0].clientY;
|
||||
const clientX = e.changedTouches[0].clientX;
|
||||
// #endif
|
||||
//计算滑动距离
|
||||
const differenceY = this.touchStartY - clientY;
|
||||
const differenceX = this.touchStartX - clientX;
|
||||
let currentTime = new Date().getTime();
|
||||
if(Math.abs(differenceY) > Math.abs(differenceX)){
|
||||
let item = this.swiperData[this.swiperIndex];
|
||||
//判断最终滑动方向Y轴
|
||||
if (differenceY < 0) {
|
||||
if(item.swiperItemIndex > 0){
|
||||
this.animationDirection = "Y";
|
||||
this.setAnimationY(this.currentY + Math.abs(differenceY), 0);
|
||||
}
|
||||
} else {
|
||||
if(item.swiperItemIndex < (item.length - 1)){
|
||||
this.animationDirection = "Y";
|
||||
this.setAnimationY(this.currentY - differenceY, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//判断最终滑动方向X轴
|
||||
if (differenceX < 0) {
|
||||
if(this.swiperIndex > 0){
|
||||
this.animationDirection = "X";
|
||||
this.setAnimationX(this.currentX + Math.abs(differenceX), 0);
|
||||
}
|
||||
} else {
|
||||
if(this.swiperIndex < this.swiperData.length - 1){
|
||||
this.animationDirection = "X";
|
||||
this.setAnimationX(this.currentX - Math.abs(differenceX), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
lastTime = currentTime;
|
||||
}
|
||||
},
|
||||
// 手指触摸动作被打断,如来电提醒,弹窗
|
||||
onSwiperTouchcancel(e) {
|
||||
// #ifdef APP-NVUE
|
||||
this.finallySlide(e.changedTouches[0].screenX, e.changedTouches[0].screenY);
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
this.finallySlide(e.changedTouches[0].clientX, e.changedTouches[0].clientY);
|
||||
// #endif
|
||||
this.fingersNumber = 0;
|
||||
},
|
||||
// 手指触摸动作结束
|
||||
onSwiperTouchend(e) {
|
||||
// #ifdef APP-NVUE
|
||||
this.finallySlide(e.changedTouches[0].screenX, e.changedTouches[0].screenY);
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
this.finallySlide(e.changedTouches[0].clientX, e.changedTouches[0].clientY);
|
||||
// #endif
|
||||
this.fingersNumber -= 1;
|
||||
},
|
||||
//最终判断滑动
|
||||
finallySlide(finallyX, finallyY) {
|
||||
if(this.canSlide && this.fingersNumber == 1){
|
||||
//手指离开的时间
|
||||
const endTime = new Date().getTime();
|
||||
//手机滑动屏幕的总花费时间
|
||||
const timeDifference = endTime - this.startTime;
|
||||
this.canSlide = false;
|
||||
//手指触摸总滑动距离
|
||||
const distanceDifferenceY = this.touchStartY - finallyY;
|
||||
if(this.animationDirection == "X"){
|
||||
const distanceDifferenceX = this.touchStartX - finallyX;
|
||||
//判断是否滑动到左边 滑动距离超过3分之一 或者 滑动时间在300毫秒并且距离在4分之一
|
||||
if (Math.abs(distanceDifferenceX) > this.screenWidth / 3 || timeDifference < 500 && Math.abs(distanceDifferenceX) > (this.screenWidth / 5)) {
|
||||
//判断最终滑动方向
|
||||
let animationTime = 360;
|
||||
if(timeDifference > 400 && timeDifference > 200){
|
||||
animationTime = timeDifference;
|
||||
}
|
||||
if (distanceDifferenceX < 0) {
|
||||
this.setAnimationX(0, animationTime, "less");
|
||||
} else {
|
||||
this.setAnimationX(-this.screenWidth, animationTime, "plus");
|
||||
}
|
||||
} else {
|
||||
this.setAnimationX(this.currentX, 360);
|
||||
}
|
||||
} else if(this.animationDirection == "Y"){
|
||||
//判断是否滑动到左边 滑动距离超过3分之一 或者 滑动时间在300毫秒并且距离在4分之一
|
||||
if (Math.abs(distanceDifferenceY) > this.screenHeight / 5 || timeDifference < 500 && Math.abs(distanceDifferenceY) > (this.screenHeight / 10)) {
|
||||
//判断最终滑动方向
|
||||
let animationTime = 360;
|
||||
if(timeDifference < 500 && timeDifference > 200){
|
||||
animationTime = timeDifference;
|
||||
}
|
||||
if(Math.abs(distanceDifferenceY) < this.screenHeight){
|
||||
let remainingRatio = (this.screenHeight - Math.abs(distanceDifferenceY)) / this.screenHeight;
|
||||
animationTime = animationTime * remainingRatio;
|
||||
if(animationTime < 150){
|
||||
animationTime = 150;
|
||||
}
|
||||
}
|
||||
if (distanceDifferenceY < 0) {
|
||||
this.setAnimationY(this.currentY + this.screenHeight, animationTime, "less");
|
||||
} else {
|
||||
this.setAnimationY(this.currentY - this.screenHeight, animationTime, "plus");
|
||||
}
|
||||
this.onChangeY();
|
||||
} else {
|
||||
let animationTime = 360;
|
||||
let remainingRatio = Math.abs(distanceDifferenceY) / this.screenHeight;
|
||||
animationTime = animationTime * remainingRatio;
|
||||
if(animationTime < 150){
|
||||
animationTime = 150;
|
||||
}
|
||||
this.setAnimationY(this.currentY, animationTime);
|
||||
}
|
||||
} else {
|
||||
this.canSlide = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/style/mixin.scss';
|
||||
.direction_swiper {
|
||||
width: 750rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.swiper_content_box {
|
||||
flex-direction: row;
|
||||
}
|
||||
.swiper_container {
|
||||
position: relative;
|
||||
}
|
||||
.swiper_item {
|
||||
width: 750rpx;
|
||||
position: absolute;
|
||||
top: 1504rpx;
|
||||
left: 0rpx;
|
||||
}
|
||||
.swiper_empty {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 750rpx;
|
||||
position: absolute;
|
||||
top: 0rpx;
|
||||
left: 0rpx;
|
||||
}
|
||||
.swiper_empty_image {
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
}
|
||||
.swiper_empty_text {
|
||||
font-size: 28rpx;
|
||||
color: #FFF;
|
||||
}
|
||||
</style>
|
||||
146
components/guide-pages/guide-pages.vue
Normal file
146
components/guide-pages/guide-pages.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<view v-if="show">
|
||||
<swiper class="guide_pages_swiper">
|
||||
<swiper-item>
|
||||
<view class="guide_pages_bg1">
|
||||
疯子读书,
|
||||
<br>
|
||||
让阅读无处不在
|
||||
</view>
|
||||
</swiper-item>
|
||||
<swiper-item>
|
||||
<view class="guide_pages_bg2">
|
||||
疯子读书,
|
||||
<br>
|
||||
让阅读无处不在
|
||||
</view>
|
||||
</swiper-item>
|
||||
<swiper-item>
|
||||
<view class="guide_pages_bg3">
|
||||
疯子读书,
|
||||
<br>
|
||||
让阅读无处不在
|
||||
</view>
|
||||
<button v-if="screenHeight > 667" class="guide_pages_close close_1624" @click="onClose">开始使用</button>
|
||||
<button v-else class="guide_pages_close close_1334" @click="onClose">开始使用</button>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState,
|
||||
mapMutations
|
||||
} from 'vuex';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
screenHeight: 667,
|
||||
show: false
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (uni.getStorageSync('guidePages') != 2) {
|
||||
let systemInfo = uni.getSystemInfoSync();
|
||||
this.screenHeight = systemInfo.screenHeight;
|
||||
this.show = true;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
uni.setStorageSync('guidePages', 2);
|
||||
this.show = false;
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.guide_pages_swiper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #000;
|
||||
z-index: 10000;
|
||||
|
||||
image {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.guide_pages_bg1 {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: url("@/static/icon/e_0ban_1.jpg") no-repeat bottom center;
|
||||
background-size: cover;
|
||||
// background-color: #4CD964;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000;
|
||||
font-size: 60rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.guide_pages_bg2 {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: url("@/static/icon/e_0ban_2.jpg") no-repeat;
|
||||
background-size: cover;
|
||||
// background-color: #007AFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000;
|
||||
font-size: 60rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.guide_pages_bg3 {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: url("@/static/icon/e_0ban_3.jpg") no-repeat;
|
||||
background-size: cover;
|
||||
// background-color: #EA552D;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000;
|
||||
font-size: 60rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.guide_pages_close {
|
||||
border: 2rpx solid #000;
|
||||
color: #000;
|
||||
line-height: 68rpx;
|
||||
height: 68rpx;
|
||||
}
|
||||
|
||||
.close_1334 {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 290rpx;
|
||||
height: 68rpx;
|
||||
transform: translateX(-50%) translateY(438rpx);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.close_1624 {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 290rpx;
|
||||
height: 68rpx;
|
||||
background-color: transparent;
|
||||
transform: translateX(-50%) translateY(412rpx);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
29
components/init-logistics.js
Normal file
29
components/init-logistics.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import Vue from 'vue';
|
||||
export const setAttribute = function(data) {
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
return data.map((item, index) => {
|
||||
Vue.set(item, 'isFirstNode', false)
|
||||
return item
|
||||
})
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const changeAttribute = function(testStrList, targetList) {
|
||||
let cacheData = targetList;
|
||||
testStrList.forEach((item, index) => {
|
||||
let result_Index = targetList.findIndex(function(f_item, f_index) {
|
||||
return String(f_item.status) == item
|
||||
})
|
||||
if (result_Index != -1) {
|
||||
cacheData[result_Index].isFirstNode = true;
|
||||
}
|
||||
})
|
||||
return cacheData;
|
||||
}
|
||||
|
||||
export default {
|
||||
setAttribute,
|
||||
changeAttribute
|
||||
}
|
||||
675
components/jyf-parser/jyf-parser.vue
Normal file
675
components/jyf-parser/jyf-parser.vue
Normal file
@@ -0,0 +1,675 @@
|
||||
<template>
|
||||
<view>
|
||||
<slot v-if="!nodes.length" />
|
||||
<!--#ifdef APP-PLUS-NVUE-->
|
||||
<web-view id="top" ref="web" :style="'margin-top:-2px;height:'+height+'px'" @onPostMessage="_message" />
|
||||
<!--#endif-->
|
||||
<!--#ifndef APP-PLUS-NVUE-->
|
||||
<view id="top" :style="showAm+(selectable?';user-select:text;-webkit-user-select:text':'')">
|
||||
<!--#ifdef H5-->
|
||||
<div :id="'rtf'+uid"></div>
|
||||
<!--#endif-->
|
||||
<!--#ifndef H5-->
|
||||
<trees :nodes="nodes" :lazy-load="lazyLoad" />
|
||||
<!--#endif-->
|
||||
</view>
|
||||
<!--#endif-->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// #ifndef H5 || APP-PLUS-NVUE
|
||||
import trees from './libs/trees';
|
||||
var cache = {},
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
fs = uni.getFileSystemManager ? uni.getFileSystemManager() : null,
|
||||
// #endif
|
||||
Parser = require('./libs/MpHtmlParser.js');
|
||||
var dom;
|
||||
// 计算 cache 的 key
|
||||
function hash(str) {
|
||||
for (var i = str.length, val = 5381; i--;)
|
||||
val += (val << 5) + str.charCodeAt(i);
|
||||
return val;
|
||||
}
|
||||
// #endif
|
||||
// #ifdef H5 || APP-PLUS-NVUE
|
||||
var rpx = uni.getSystemInfoSync().screenWidth / 750,
|
||||
cfg = require('./libs/config.js');
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
var weexDom = weex.requireModule('dom');
|
||||
// #endif
|
||||
/**
|
||||
* Parser 富文本组件
|
||||
* @tutorial https://github.com/jin-yufeng/Parser
|
||||
* @property {String|Array} html 富文本数据
|
||||
* @property {Boolean} autopause 是否在播放一个视频时自动暂停其他视频
|
||||
* @property {Boolean} autoscroll 是否自动给所有表格添加一个滚动层
|
||||
* @property {Boolean} autosetTitle 是否自动将 title 标签中的内容设置到页面标题
|
||||
* @property {Number} compress 压缩等级
|
||||
* @property {String} domain 图片、视频等链接的主域名
|
||||
* @property {Boolean} lazyLoad 是否开启图片懒加载
|
||||
* @property {Boolean} selectable 是否开启长按复制
|
||||
* @property {Object} tagStyle 标签的默认样式
|
||||
* @property {Boolean} showWithAnimation 是否使用渐显动画
|
||||
* @property {Boolean} useAnchor 是否使用锚点
|
||||
* @property {Boolean} useCache 是否缓存解析结果
|
||||
* @event {Function} parse 解析完成事件
|
||||
* @event {Function} load dom 加载完成事件
|
||||
* @event {Function} ready 所有图片加载完毕事件
|
||||
* @event {Function} error 错误事件
|
||||
* @event {Function} imgtap 图片点击事件
|
||||
* @event {Function} linkpress 链接点击事件
|
||||
* @example <jyf-parser :html="html"></jyf-parser>
|
||||
* @author JinYufeng
|
||||
* @version 20200513
|
||||
* @listens MIT
|
||||
*/
|
||||
export default {
|
||||
name: 'parser',
|
||||
data() {
|
||||
return {
|
||||
// #ifdef H5
|
||||
uid: this._uid,
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
height: 1,
|
||||
// #endif
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
showAm: '',
|
||||
imgs: [],
|
||||
// #endif
|
||||
nodes: []
|
||||
}
|
||||
},
|
||||
// #ifndef H5 || APP-PLUS-NVUE
|
||||
components: {
|
||||
trees
|
||||
},
|
||||
// #endif
|
||||
props: {
|
||||
'html': null,
|
||||
'autopause': {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
'autoscroll': Boolean,
|
||||
'autosetTitle': {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// #ifndef H5 || APP-PLUS-NVUE
|
||||
'compress': Number,
|
||||
'useCache': Boolean,
|
||||
// #endif
|
||||
'domain': String,
|
||||
'lazyLoad': Boolean,
|
||||
'selectable': Boolean,
|
||||
'tagStyle': Object,
|
||||
'showWithAnimation': Boolean,
|
||||
'useAnchor': Boolean
|
||||
},
|
||||
watch: {
|
||||
html(html) {
|
||||
this.setContent(html);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 图片数组
|
||||
this.imgList = [];
|
||||
this.imgList.each = function(f) {
|
||||
for (var i = 0, len = this.length; i < len; i++)
|
||||
this.setItem(i, f(this[i], i, this));
|
||||
}
|
||||
this.imgList.setItem = function(i, src) {
|
||||
if (i == void 0 || !src) return;
|
||||
// #ifndef MP-ALIPAY || APP-PLUS
|
||||
// 去重
|
||||
if (src.indexOf('http') == 0 && this.includes(src)) {
|
||||
var newSrc = '';
|
||||
for (var j = 0, c; c = src[j]; j++) {
|
||||
if (c == '/' && src[j - 1] != '/' && src[j + 1] != '/') break;
|
||||
newSrc += Math.random() > 0.5 ? c.toUpperCase() : c;
|
||||
}
|
||||
newSrc += src.substr(j);
|
||||
return this[i] = newSrc;
|
||||
}
|
||||
// #endif
|
||||
this[i] = src;
|
||||
// 暂存 data src
|
||||
if (src.includes('data:image')) {
|
||||
var filePath, info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
|
||||
if (!info) return;
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
|
||||
fs && fs.writeFile({
|
||||
filePath,
|
||||
data: info[3],
|
||||
encoding: info[2],
|
||||
success: () => this[i] = filePath
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
filePath = `_doc/parser_tmp/${Date.now()}.${info[1]}`;
|
||||
var bitmap = new plus.nativeObj.Bitmap();
|
||||
bitmap.loadBase64Data(src, () => {
|
||||
bitmap.save(filePath, {}, () => {
|
||||
bitmap.clear()
|
||||
this[i] = filePath;
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
// #ifdef H5
|
||||
this.document = document.getElementById('rtf' + this._uid);
|
||||
// #endif
|
||||
// #ifndef H5 || APP-PLUS-NVUE
|
||||
if (dom) this.document = new dom(this);
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
this.document = this.$refs.web;
|
||||
this.$nextTick(() => {
|
||||
// #endif
|
||||
if (this.html) this.setContent(this.html);
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
beforeDestroy() {
|
||||
// #ifdef H5
|
||||
if (this._observer) this._observer.disconnect();
|
||||
// #endif
|
||||
this.imgList.each(src => {
|
||||
// #ifdef APP-PLUS
|
||||
if (src && src.includes('_doc')) {
|
||||
plus.io.resolveLocalFileSystemURL(src, entry => {
|
||||
entry.remove();
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
if (src && src.includes(uni.env.USER_DATA_PATH))
|
||||
fs && fs.unlink({
|
||||
filePath: src
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
clearInterval(this._timer);
|
||||
},
|
||||
methods: {
|
||||
// #ifdef H5 || APP-PLUS-NVUE
|
||||
_Dom2Str(nodes) {
|
||||
var str = '';
|
||||
for (var node of nodes) {
|
||||
if (node.type == 'text')
|
||||
str += node.text;
|
||||
else {
|
||||
str += ('<' + node.name);
|
||||
for (var attr in node.attrs || {})
|
||||
str += (' ' + attr + '="' + node.attrs[attr] + '"');
|
||||
if (!node.children || !node.children.length) str += '>';
|
||||
else str += ('>' + this._Dom2Str(node.children) + '</' + node.name + '>');
|
||||
}
|
||||
}
|
||||
return str;
|
||||
},
|
||||
_handleHtml(html, append) {
|
||||
if (typeof html != 'string') html = this._Dom2Str(html.nodes || html);
|
||||
if (!append) {
|
||||
// 处理 tag-style 和 userAgentStyles
|
||||
var style = '<style>@keyframes show{0%{opacity:0}100%{opacity:1}}img{max-width:100%}';
|
||||
for (var item in cfg.userAgentStyles)
|
||||
style += `${item}{${cfg.userAgentStyles[item]}}`;
|
||||
for (item in this.tagStyle)
|
||||
style += `${item}{${this.tagStyle[item]}}`;
|
||||
style += '</style>';
|
||||
html = style + html;
|
||||
}
|
||||
// 处理 rpx
|
||||
if (html.includes('rpx'))
|
||||
html = html.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * rpx + 'px');
|
||||
return html;
|
||||
},
|
||||
// #endif
|
||||
setContent(html, append) {
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
if (!html)
|
||||
return this.height = 1;
|
||||
if (append)
|
||||
this.$refs.web.evalJs("var d=document.createElement('div');d.innerHTML='" + html.replace(/'/g, "\\'") +
|
||||
"';document.getElementById('parser').appendChild(d)");
|
||||
else {
|
||||
html =
|
||||
'<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1' +
|
||||
(this.selectable ? '' : ',user-scalable=no') + '"><base href="' + this.domain + '"><div id="parser">' + this._handleHtml(
|
||||
html) +
|
||||
'</div><script>"use strict";function post(n){if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){var t={data:[n]};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(t):window.__dcloud_weex_.postMessage(JSON.stringify(t))}}function waitReady(){return new Promise(function(e){var t=document.getElementById("parser"),r=t.scrollHeight,n=setInterval(function(){r==t.scrollHeight?(clearInterval(n),e(r)):r=t.scrollHeight},500)})}' +
|
||||
(this.showWithAnimation ? 'document.body.style.animation="show .5s",' : '') +
|
||||
'setTimeout(function(){post({action:"load",text:document.body.innerText,height:document.getElementById("parser").scrollHeight+16})},50);</' +
|
||||
'script>';
|
||||
this.$refs.web.evalJs("document.write('" + html.replace(/'/g, "\\'") + "');document.close()");
|
||||
}
|
||||
this.$refs.web.evalJs(
|
||||
'var e=document.getElementsByTagName("title");e.length&&post({action:"getTitle",title:e[0].innerText});for(var t,o=document.getElementsByTagName("style"),r=0;t=o[r];r++)t.innerHTML=t.innerHTML.replace(/body/g,"#parser");for(var n,i=document.getElementsByTagName("img"),a=[],s=0,c=0;n=i[s];s++)n.onerror=function(){post({action:"error",source:"img",target:this})},n.hasAttribute("ignore")||"A"==n.parentElement.nodeName||(n.i=c++,a.push(n.src),n.onclick=function(){post({action:"preview",img:{i:this.i,src:this.src}})});post({action:"getImgList",imgList:a});for(var m,g=document.getElementsByTagName("a"),l=0;m=g[l];l++)m.onclick=function(){var e,t=this.getAttribute("href");if("#"==t[0]){var o=document.getElementById(t.substr(1));o&&(e=o.offsetTop)}return post({action:"linkpress",href:t,offset:e}),!1};for(var u,d=document.getElementsByTagName("video"),f=0;u=d[f];f++)u.style.maxWidth="100%",u.onerror=function(){post({action:"error",source:"video",target:this})}' +
|
||||
(this.autopause ? ',u.onplay=function(){for(var e,t=0;e=d[t];t++)e!=this&&e.pause()}' : '') +
|
||||
';for(var p,h=document.getElementsByTagName("audio"),v=0;p=h[v];v++)p.onerror=function(){post({action:"error",source:"audio",target:this})};' +
|
||||
(this.autoscroll ?
|
||||
'for(var y,T=document.getElementsByTagName("table"),E=0;y=T[E];E++){var N=document.createElement("div");N.style.overflow="scroll",y.parentNode.replaceChild(N,y),N.appendChild(y)}' :
|
||||
'') + ';waitReady().then(function(e){post({action:"ready",height:e+16})})'
|
||||
)
|
||||
this.nodes = [1];
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
if (!html) {
|
||||
if (this.rtf && !append) this.rtf.parentNode.removeChild(this.rtf);
|
||||
return;
|
||||
}
|
||||
var div = document.createElement('div');
|
||||
if (!append) {
|
||||
if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
|
||||
this.rtf = div;
|
||||
} else {
|
||||
if (!this.rtf) this.rtf = div;
|
||||
else this.rtf.appendChild(div);
|
||||
}
|
||||
div.innerHTML = this._handleHtml(html, append);
|
||||
for (var styles = this.rtf.getElementsByTagName('style'), i = 0, style; style = styles[i++];) {
|
||||
style.innerHTML = style.innerHTML.replace(/body/g, '#rtf' + this._uid);
|
||||
style.setAttribute('scoped', 'true');
|
||||
}
|
||||
// 懒加载
|
||||
if (!this._observer && this.lazyLoad && IntersectionObserver) {
|
||||
this._observer = new IntersectionObserver(changes => {
|
||||
for (let item, i = 0; item = changes[i++];) {
|
||||
if (item.isIntersecting) {
|
||||
item.target.src = item.target.getAttribute('data-src');
|
||||
item.target.removeAttribute('data-src');
|
||||
this._observer.unobserve(item.target);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
rootMargin: '500px 0px 500px 0px'
|
||||
})
|
||||
}
|
||||
var _ts = this;
|
||||
// 获取标题
|
||||
var title = this.rtf.getElementsByTagName('title');
|
||||
if (title.length && this.autosetTitle)
|
||||
uni.setNavigationBarTitle({
|
||||
title: title[0].innerText
|
||||
})
|
||||
// 图片处理
|
||||
this.imgList.length = 0;
|
||||
var imgs = this.rtf.getElementsByTagName('img');
|
||||
for (let i = 0, j = 0, img; img = imgs[i]; i++) {
|
||||
var src = img.getAttribute('src');
|
||||
if (this.domain && src) {
|
||||
if (src[0] == '/') {
|
||||
if (src[1] == '/')
|
||||
img.src = (this.domain.includes('://') ? this.domain.split('://')[0] : '') + ':' + src;
|
||||
else img.src = this.domain + src;
|
||||
} else if (!src.includes('://')) img.src = this.domain + '/' + src;
|
||||
}
|
||||
if (!img.hasAttribute('ignore') && img.parentElement.nodeName != 'A') {
|
||||
img.i = j++;
|
||||
_ts.imgList.push(img.src || img.getAttribute('data-src'));
|
||||
img.onclick = function() {
|
||||
var preview = true;
|
||||
this.ignore = () => preview = false;
|
||||
_ts.$emit('imgtap', this);
|
||||
if (preview) {
|
||||
uni.previewImage({
|
||||
current: this.i,
|
||||
urls: _ts.imgList
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
img.onerror = function() {
|
||||
_ts.$emit('error', {
|
||||
source: 'img',
|
||||
target: this,
|
||||
context: {
|
||||
setSrc: src => this.src = src
|
||||
}
|
||||
});
|
||||
}
|
||||
if (_ts.lazyLoad && this._observer && img.src && img.i != 0) {
|
||||
img.setAttribute('data-src', img.src);
|
||||
img.removeAttribute('src');
|
||||
this._observer.observe(img);
|
||||
}
|
||||
}
|
||||
// 链接处理
|
||||
var links = this.rtf.getElementsByTagName('a');
|
||||
for (var link of links) {
|
||||
link.onclick = function() {
|
||||
var jump = true,
|
||||
href = this.getAttribute('href');
|
||||
_ts.$emit('linkpress', {
|
||||
href,
|
||||
ignore: () => jump = false
|
||||
});
|
||||
if (jump && href) {
|
||||
if (href[0] == '#') {
|
||||
if (_ts.useAnchor) {
|
||||
_ts.navigateTo({
|
||||
id: href.substr(1)
|
||||
})
|
||||
}
|
||||
} else if (href.indexOf('http') == 0 || href.indexOf('//') == 0)
|
||||
return true;
|
||||
else {
|
||||
uni.navigateTo({
|
||||
url: href
|
||||
})
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 视频处理
|
||||
var videos = this.rtf.getElementsByTagName('video');
|
||||
_ts.videoContexts = videos;
|
||||
for (let video, i = 0; video = videos[i++];) {
|
||||
video.style.maxWidth = '100%';
|
||||
video.onerror = function() {
|
||||
_ts.$emit('error', {
|
||||
source: 'video',
|
||||
target: this,
|
||||
context: this
|
||||
});
|
||||
}
|
||||
video.onplay = function() {
|
||||
if (_ts.autopause)
|
||||
for (let item, i = 0; item = _ts.videoContexts[i++];)
|
||||
if (item != this) item.pause();
|
||||
}
|
||||
}
|
||||
// 音频处理
|
||||
var audios = this.rtf.getElementsByTagName('audio');
|
||||
for (var audio of audios)
|
||||
audio.onerror = function() {
|
||||
_ts.$emit('error', {
|
||||
source: 'audio',
|
||||
target: this,
|
||||
context: this
|
||||
});
|
||||
}
|
||||
// 表格处理
|
||||
if (this.autoscroll) {
|
||||
var tables = this.rtf.getElementsByTagName('table');
|
||||
for (var table of tables) {
|
||||
var div = document.createElement('div');
|
||||
div.style.overflow = 'scroll';
|
||||
table.parentNode.replaceChild(div, table);
|
||||
div.appendChild(table);
|
||||
}
|
||||
}
|
||||
if (!append) this.document.appendChild(this.rtf);
|
||||
this.$nextTick(() => {
|
||||
this.nodes = [1];
|
||||
this.$emit('load');
|
||||
});
|
||||
setTimeout(() => this.showAm = '', 500);
|
||||
// #endif
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
// #ifndef H5
|
||||
var nodes;
|
||||
if (!html)
|
||||
return this.nodes = [];
|
||||
else if (typeof html == 'string') {
|
||||
let parser = new Parser(html, this);
|
||||
// 缓存读取
|
||||
if (this.useCache) {
|
||||
var hashVal = hash(html);
|
||||
if (cache[hashVal])
|
||||
nodes = cache[hashVal];
|
||||
else {
|
||||
nodes = parser.parse();
|
||||
cache[hashVal] = nodes;
|
||||
}
|
||||
} else nodes = parser.parse();
|
||||
this.$emit('parse', nodes);
|
||||
} else if (Object.prototype.toString.call(html) == '[object Array]') {
|
||||
// 非本插件产生的 array 需要进行一些转换
|
||||
if (html.length && html[0].PoweredBy != 'Parser') {
|
||||
let parser = new Parser(html, this);
|
||||
(function f(ns) {
|
||||
for (var i = 0, n; n = ns[i]; i++) {
|
||||
if (n.type == 'text') continue;
|
||||
n.attrs = n.attrs || {};
|
||||
for (var item in n.attrs)
|
||||
if (typeof n.attrs[item] != 'string') n.attrs[item] = n.attrs[item].toString();
|
||||
parser.matchAttr(n, parser);
|
||||
if (n.children && n.children.length) {
|
||||
parser.STACK.push(n);
|
||||
f(n.children);
|
||||
parser.popNode(parser.STACK.pop());
|
||||
} else n.children = void 0;
|
||||
}
|
||||
})(html);
|
||||
}
|
||||
nodes = html;
|
||||
} else if (typeof html == 'object' && html.nodes) {
|
||||
nodes = html.nodes;
|
||||
console.warn('错误的 html 类型:object 类型已废弃');
|
||||
} else
|
||||
return console.warn('错误的 html 类型:' + typeof html);
|
||||
if (append) this.nodes = this.nodes.concat(nodes);
|
||||
else this.nodes = nodes;
|
||||
if (nodes.length && nodes[0].title && this.autosetTitle)
|
||||
uni.setNavigationBarTitle({
|
||||
title: nodes[0].title
|
||||
})
|
||||
this.$nextTick(() => {
|
||||
this.imgList.length = 0;
|
||||
this.videoContexts = [];
|
||||
this.$emit('load');
|
||||
})
|
||||
// #endif
|
||||
var height;
|
||||
clearInterval(this._timer);
|
||||
this._timer = setInterval(() => {
|
||||
// #ifdef H5
|
||||
this.rect = this.rtf.getBoundingClientRect();
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
// #ifdef APP-PLUS
|
||||
uni.createSelectorQuery().in(this)
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
this.createSelectorQuery()
|
||||
// #endif
|
||||
.select('#top').boundingClientRect().exec(res => {
|
||||
this.rect = res[0];
|
||||
// #endif
|
||||
if (this.rect.height == height) {
|
||||
this.$emit('ready', this.rect)
|
||||
clearInterval(this._timer);
|
||||
}
|
||||
height = this.rect.height;
|
||||
// #ifndef H5
|
||||
});
|
||||
// #endif
|
||||
}, 350);
|
||||
if (this.showWithAnimation && !append) this.showAm = 'animation:show .5s';
|
||||
// #endif
|
||||
},
|
||||
getText(ns = this.nodes) {
|
||||
var txt = '';
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
txt = this._text;
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
txt = this.rtf.innerText;
|
||||
// #endif
|
||||
// #ifndef H5 || APP-PLUS-NVUE
|
||||
for (var i = 0, n; n = ns[i++];) {
|
||||
if (n.type == 'text') txt += n.text.replace(/ /g, '\u00A0').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/&/g, '&');
|
||||
else if (n.type == 'br') txt += '\n';
|
||||
else {
|
||||
// 块级标签前后加换行
|
||||
var block = n.name == 'p' || n.name == 'div' || n.name == 'tr' || n.name == 'li' || (n.name[0] == 'h' && n.name[1] >
|
||||
'0' && n.name[1] < '7');
|
||||
if (block && txt && txt[txt.length - 1] != '\n') txt += '\n';
|
||||
if (n.children) txt += this.getText(n.children);
|
||||
if (block && txt[txt.length - 1] != '\n') txt += '\n';
|
||||
else if (n.name == 'td' || n.name == 'th') txt += '\t';
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
return txt;
|
||||
},
|
||||
navigateTo(obj) {
|
||||
if (!this.useAnchor)
|
||||
return obj.fail && obj.fail({
|
||||
errMsg: 'Anchor is disabled'
|
||||
})
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
if (!obj.id)
|
||||
weexDom.scrollToElement(this.$refs.web);
|
||||
else
|
||||
this.$refs.web.evalJs('var pos=document.getElementById("' + obj.id +
|
||||
'");if(pos)post({action:"linkpress",href:"#",offset:pos.offsetTop+' + (obj.offset || 0) + '})');
|
||||
obj.success && obj.success({
|
||||
errMsg: 'pageScrollTo:ok'
|
||||
});
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
if (!obj.id) {
|
||||
window.scrollTo(0, this.rtf.offsetTop);
|
||||
return obj.success && obj.success({
|
||||
errMsg: 'pageScrollTo:ok'
|
||||
});
|
||||
}
|
||||
var target = document.getElementById(obj.id);
|
||||
if (!target) return obj.fail && obj.fail({
|
||||
errMsg: 'Label not found'
|
||||
});
|
||||
obj.scrollTop = this.rtf.offsetTop + target.offsetTop + (obj.offset || 0);
|
||||
uni.pageScrollTo(obj);
|
||||
// #endif
|
||||
// #ifndef H5 || APP-PLUS-NVUE
|
||||
var Scroll = (selector, component) => {
|
||||
uni.createSelectorQuery().in(component ? component : this).select(selector).boundingClientRect().selectViewport()
|
||||
.scrollOffset()
|
||||
.exec(res => {
|
||||
if (!res || !res[0])
|
||||
return obj.fail && obj.fail({
|
||||
errMsg: 'Label not found'
|
||||
});
|
||||
obj.scrollTop = res[1].scrollTop + res[0].top + (obj.offset || 0);
|
||||
uni.pageScrollTo(obj);
|
||||
})
|
||||
}
|
||||
if (!obj.id) Scroll('#top');
|
||||
else {
|
||||
// #ifndef MP-BAIDU || MP-ALIPAY || APP-PLUS
|
||||
Scroll('#top >>> #' + obj.id + ', #top >>> .' + obj.id);
|
||||
// #endif
|
||||
// #ifdef MP-BAIDU || MP-ALIPAY || APP-PLUS
|
||||
for (var anchor of this.anchors)
|
||||
if (anchor.id == obj.id)
|
||||
Scroll('#' + obj.id + ', .' + obj.id, anchor.node);
|
||||
// #endif
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
getVideoContext(id) {
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
if (!id) return this.videoContexts;
|
||||
else
|
||||
for (var i = this.videoContexts.length; i--;)
|
||||
if (this.videoContexts[i].id == id) return this.videoContexts[i];
|
||||
// #endif
|
||||
},
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
_message(e) {
|
||||
// 接收 web-view 消息
|
||||
var data = e.detail.data[0];
|
||||
if (data.action == 'load') {
|
||||
this.$emit('load');
|
||||
this.height = data.height;
|
||||
this._text = data.text;
|
||||
} else if (data.action == 'getTitle') {
|
||||
if (this.autosetTitle)
|
||||
uni.setNavigationBarTitle({
|
||||
title: data.title
|
||||
})
|
||||
} else if (data.action == 'getImgList') {
|
||||
this.imgList.length = 0;
|
||||
for (var i = data.imgList.length; i--;)
|
||||
this.imgList.setItem(i, data.imgList[i]);
|
||||
} else if (data.action == 'preview') {
|
||||
var preview = true;
|
||||
data.img.ignore = () => preview = false;
|
||||
this.$emit('imgtap', data.img);
|
||||
if (preview)
|
||||
uni.previewImage({
|
||||
current: data.img.i,
|
||||
urls: this.imgList
|
||||
})
|
||||
} else if (data.action == 'linkpress') {
|
||||
var jump = true,
|
||||
href = data.href;
|
||||
this.$emit('linkpress', {
|
||||
href,
|
||||
ignore: () => jump = false
|
||||
})
|
||||
if (jump && href) {
|
||||
if (href[0] == '#') {
|
||||
if (this.useAnchor)
|
||||
weexDom.scrollToElement(this.$refs.web, {
|
||||
offset: data.offset
|
||||
})
|
||||
} else if (href.includes('://'))
|
||||
plus.runtime.openWeb(href);
|
||||
else
|
||||
uni.navigateTo({
|
||||
url: href
|
||||
})
|
||||
}
|
||||
} else if (data.action == 'error')
|
||||
this.$emit('error', {
|
||||
source: data.source,
|
||||
target: data.target
|
||||
})
|
||||
else if (data.action == 'ready') {
|
||||
this.height = data.height;
|
||||
this.$nextTick(() => {
|
||||
uni.createSelectorQuery().in(this).select('#top').boundingClientRect().exec(res => {
|
||||
this.rect = res[0];
|
||||
this.$emit('ready', res[0]);
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes show {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* #ifdef MP-WEIXIN */
|
||||
:host {
|
||||
display: block;
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
99
components/jyf-parser/libs/CssHandler.js
Normal file
99
components/jyf-parser/libs/CssHandler.js
Normal file
@@ -0,0 +1,99 @@
|
||||
var cfg = require('./config.js'),
|
||||
isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||||
class CssHandler {
|
||||
constructor(tagStyle) {
|
||||
var styles = Object.assign({}, cfg.userAgentStyles);
|
||||
for (var item in tagStyle)
|
||||
styles[item] = (styles[item] ? styles[item] + ';' : '') + tagStyle[item];
|
||||
this.styles = styles;
|
||||
}
|
||||
getStyle(data) {
|
||||
this.styles = new CssParser(data, this.styles).parse();
|
||||
}
|
||||
match(name, attrs) {
|
||||
var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : '';
|
||||
if (attrs.class) {
|
||||
var items = attrs.class.split(' ');
|
||||
for (var i = 0, item; item = items[i]; i++)
|
||||
if (tmp = this.styles['.' + item])
|
||||
matched += tmp + ';';
|
||||
}
|
||||
if (tmp = this.styles['#' + attrs.id])
|
||||
matched += tmp + ';';
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
module.exports = CssHandler;
|
||||
class CssParser {
|
||||
constructor(data, init) {
|
||||
this.data = data;
|
||||
this.floor = 0;
|
||||
this.i = 0;
|
||||
this.list = [];
|
||||
this.res = init;
|
||||
this.state = this.Space;
|
||||
}
|
||||
parse() {
|
||||
for (var c; c = this.data[this.i]; this.i++)
|
||||
this.state(c);
|
||||
return this.res;
|
||||
}
|
||||
section() {
|
||||
return this.data.substring(this.start, this.i);
|
||||
}
|
||||
// 状态机
|
||||
Space(c) {
|
||||
if (c == '.' || c == '#' || isLetter(c)) {
|
||||
this.start = this.i;
|
||||
this.state = this.Name;
|
||||
} else if (c == '/' && this.data[this.i + 1] == '*')
|
||||
this.Comment();
|
||||
else if (!cfg.blankChar[c] && c != ';')
|
||||
this.state = this.Ignore;
|
||||
}
|
||||
Comment() {
|
||||
this.i = this.data.indexOf('*/', this.i) + 1;
|
||||
if (!this.i) this.i = this.data.length;
|
||||
this.state = this.Space;
|
||||
}
|
||||
Ignore(c) {
|
||||
if (c == '{') this.floor++;
|
||||
else if (c == '}' && !--this.floor) this.state = this.Space;
|
||||
}
|
||||
Name(c) {
|
||||
if (cfg.blankChar[c]) {
|
||||
this.list.push(this.section());
|
||||
this.state = this.NameSpace;
|
||||
} else if (c == '{') {
|
||||
this.list.push(this.section());
|
||||
this.Content();
|
||||
} else if (c == ',') {
|
||||
this.list.push(this.section());
|
||||
this.Comma();
|
||||
} else if (!isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_')
|
||||
this.state = this.Ignore;
|
||||
}
|
||||
NameSpace(c) {
|
||||
if (c == '{') this.Content();
|
||||
else if (c == ',') this.Comma();
|
||||
else if (!cfg.blankChar[c]) this.state = this.Ignore;
|
||||
}
|
||||
Comma() {
|
||||
while (cfg.blankChar[this.data[++this.i]]);
|
||||
if (this.data[this.i] == '{') this.Content();
|
||||
else {
|
||||
this.start = this.i--;
|
||||
this.state = this.Name;
|
||||
}
|
||||
}
|
||||
Content() {
|
||||
this.start = ++this.i;
|
||||
if ((this.i = this.data.indexOf('}', this.i)) == -1) this.i = this.data.length;
|
||||
var content = this.section();
|
||||
for (var i = 0, item; item = this.list[i++];)
|
||||
if (this.res[item]) this.res[item] += ';' + content;
|
||||
else this.res[item] = content;
|
||||
this.list = [];
|
||||
this.state = this.Space;
|
||||
}
|
||||
}
|
||||
534
components/jyf-parser/libs/MpHtmlParser.js
Normal file
534
components/jyf-parser/libs/MpHtmlParser.js
Normal file
@@ -0,0 +1,534 @@
|
||||
/**
|
||||
* html 解析器
|
||||
* @tutorial https://github.com/jin-yufeng/Parser
|
||||
* @version 20200513
|
||||
* @author JinYufeng
|
||||
* @listens MIT
|
||||
*/
|
||||
var cfg = require('./config.js'),
|
||||
blankChar = cfg.blankChar,
|
||||
CssHandler = require('./CssHandler.js'),
|
||||
windowWidth = uni.getSystemInfoSync().windowWidth;
|
||||
var emoji;
|
||||
class MpHtmlParser {
|
||||
constructor(data, options = {}) {
|
||||
this.attrs = {};
|
||||
this.CssHandler = new CssHandler(options.tagStyle, windowWidth);
|
||||
this.data = data;
|
||||
this.domain = options.domain;
|
||||
this.DOM = [];
|
||||
this.i = this.start = this.audioNum = this.imgNum = this.videoNum = 0;
|
||||
options.prot = (this.domain || '').includes('://') ? this.domain.split('://')[0] : 'http';
|
||||
this.options = options;
|
||||
this.state = this.Text;
|
||||
this.STACK = [];
|
||||
}
|
||||
parse() {
|
||||
if (emoji) this.data = emoji.parseEmoji(this.data);
|
||||
for (var c; c = this.data[this.i]; this.i++)
|
||||
this.state(c);
|
||||
if (this.state == this.Text) this.setText();
|
||||
while (this.STACK.length) this.popNode(this.STACK.pop());
|
||||
if (this.DOM.length) {
|
||||
this.DOM[0].PoweredBy = 'Parser';
|
||||
if (this.title) this.DOM[0].title = this.title;
|
||||
}
|
||||
return this.DOM;
|
||||
}
|
||||
// 设置属性
|
||||
setAttr() {
|
||||
var name = this.attrName.toLowerCase();
|
||||
if (cfg.trustAttrs[name]) {
|
||||
var val = this.attrVal;
|
||||
if (val) {
|
||||
if (name == 'src') this.attrs[name] = this.getUrl(this.decode(val, 'amp'));
|
||||
else if (name == 'href' || name == 'style') this.attrs[name] = this.decode(val, 'amp');
|
||||
else this.attrs[name] = val;
|
||||
} else if (cfg.boolAttrs[name]) this.attrs[name] = 'T';
|
||||
}
|
||||
this.attrVal = '';
|
||||
while (blankChar[this.data[this.i]]) this.i++;
|
||||
if (this.isClose()) this.setNode();
|
||||
else {
|
||||
this.start = this.i;
|
||||
this.state = this.AttrName;
|
||||
}
|
||||
}
|
||||
// 设置文本节点
|
||||
setText() {
|
||||
var back, text = this.section();
|
||||
if (!text) return;
|
||||
text = (cfg.onText && cfg.onText(text, () => back = true)) || text;
|
||||
if (back) {
|
||||
this.data = this.data.substr(0, this.start) + text + this.data.substr(this.i);
|
||||
let j = this.start + text.length;
|
||||
for (this.i = this.start; this.i < j; this.i++) this.state(this.data[this.i]);
|
||||
return;
|
||||
}
|
||||
if (!this.pre) {
|
||||
// 合并空白符
|
||||
var tmp = [];
|
||||
for (let i = text.length, c; c = text[--i];)
|
||||
if (!blankChar[c] || (!blankChar[tmp[0]] && (c = ' '))) tmp.unshift(c);
|
||||
text = tmp.join('');
|
||||
}
|
||||
this.siblings().push({
|
||||
type: 'text',
|
||||
text: this.decode(text)
|
||||
});
|
||||
}
|
||||
// 设置元素节点
|
||||
setNode() {
|
||||
var node = {
|
||||
name: this.tagName.toLowerCase(),
|
||||
attrs: this.attrs
|
||||
},
|
||||
close = cfg.selfClosingTags[node.name];
|
||||
this.attrs = {};
|
||||
if (!cfg.ignoreTags[node.name]) {
|
||||
this.matchAttr(node);
|
||||
if (!close) {
|
||||
node.children = [];
|
||||
if (node.name == 'pre' && cfg.highlight) {
|
||||
this.remove(node);
|
||||
this.pre = node.pre = true;
|
||||
}
|
||||
this.siblings().push(node);
|
||||
this.STACK.push(node);
|
||||
} else if (!cfg.filter || cfg.filter(node, this) != false)
|
||||
this.siblings().push(node);
|
||||
} else {
|
||||
if (!close) this.remove(node);
|
||||
else if (node.name == 'source') {
|
||||
var parent = this.parent();
|
||||
if (parent && (parent.name == 'video' || parent.name == 'audio') && node.attrs.src)
|
||||
parent.attrs.source.push(node.attrs.src);
|
||||
} else if (node.name == 'base' && !this.domain) this.domain = node.attrs.href;
|
||||
}
|
||||
if (this.data[this.i] == '/') this.i++;
|
||||
this.start = this.i + 1;
|
||||
this.state = this.Text;
|
||||
}
|
||||
// 移除标签
|
||||
remove(node) {
|
||||
var name = node.name,
|
||||
j = this.i;
|
||||
// 处理 svg
|
||||
var handleSvg = () => {
|
||||
var src = this.data.substring(j, this.i + 1);
|
||||
if (!node.attrs.xmlns) src = ' xmlns="http://www.w3.org/2000/svg"' + src;
|
||||
var i = j;
|
||||
while (this.data[j] != '<') j--;
|
||||
src = this.data.substring(j, i) + src;
|
||||
var parent = this.parent();
|
||||
if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline'))
|
||||
parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style;
|
||||
this.siblings().push({
|
||||
name: 'img',
|
||||
attrs: {
|
||||
src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
|
||||
ignore: 'T'
|
||||
}
|
||||
})
|
||||
}
|
||||
if (node.name == 'svg' && this.data[j] == '/') return handleSvg(this.i++);
|
||||
while (1) {
|
||||
if ((this.i = this.data.indexOf('</', this.i + 1)) == -1) {
|
||||
if (name == 'pre' || name == 'svg') this.i = j;
|
||||
else this.i = this.data.length;
|
||||
return;
|
||||
}
|
||||
this.start = (this.i += 2);
|
||||
while (!blankChar[this.data[this.i]] && !this.isClose()) this.i++;
|
||||
if (this.section().toLowerCase() == name) {
|
||||
// 代码块高亮
|
||||
if (name == 'pre') {
|
||||
this.data = this.data.substr(0, j + 1) + cfg.highlight(this.data.substring(j + 1, this.i - 5), node.attrs) +
|
||||
this.data.substr(this.i - 5);
|
||||
return this.i = j;
|
||||
} else if (name == 'style')
|
||||
this.CssHandler.getStyle(this.data.substring(j + 1, this.i - 7));
|
||||
else if (name == 'title')
|
||||
this.title = this.data.substring(j + 1, this.i - 7);
|
||||
if ((this.i = this.data.indexOf('>', this.i)) == -1) this.i = this.data.length;
|
||||
if (name == 'svg') handleSvg();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理属性
|
||||
matchAttr(node) {
|
||||
var attrs = node.attrs,
|
||||
style = this.CssHandler.match(node.name, attrs, node) + (attrs.style || ''),
|
||||
styleObj = {};
|
||||
if (attrs.id) {
|
||||
if (this.options.compress & 1) attrs.id = void 0;
|
||||
else if (this.options.useAnchor) this.bubble();
|
||||
}
|
||||
if ((this.options.compress & 2) && attrs.class) attrs.class = void 0;
|
||||
switch (node.name) {
|
||||
case 'a':
|
||||
case 'ad':
|
||||
this.bubble();
|
||||
break;
|
||||
// #ifdef APP-PLUS
|
||||
case 'iframe':
|
||||
case 'embed':
|
||||
this.bubble();
|
||||
break;
|
||||
// #endif
|
||||
case 'font':
|
||||
if (attrs.color) {
|
||||
styleObj['color'] = attrs.color;
|
||||
attrs.color = void 0;
|
||||
}
|
||||
if (attrs.face) {
|
||||
styleObj['font-family'] = attrs.face;
|
||||
attrs.face = void 0;
|
||||
}
|
||||
if (attrs.size) {
|
||||
var size = parseInt(attrs.size);
|
||||
if (size < 1) size = 1;
|
||||
else if (size > 7) size = 7;
|
||||
var map = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
|
||||
styleObj['font-size'] = map[size - 1];
|
||||
attrs.size = void 0;
|
||||
}
|
||||
break;
|
||||
case 'video':
|
||||
case 'audio':
|
||||
if (!attrs.id) attrs.id = node.name + (++this[`${node.name}Num`]);
|
||||
else this[`${node.name}Num`]++;
|
||||
if (node.name == 'video') {
|
||||
if (this.videoNum > 3)
|
||||
node.lazyLoad = 1;
|
||||
if (attrs.width) {
|
||||
styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px');
|
||||
attrs.width = void 0;
|
||||
}
|
||||
if (attrs.height) {
|
||||
styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px');
|
||||
attrs.height = void 0;
|
||||
}
|
||||
}
|
||||
attrs.source = [];
|
||||
if (attrs.src) attrs.source.push(attrs.src);
|
||||
if (!attrs.controls && !attrs.autoplay)
|
||||
console.warn(`存在没有 controls 属性的 ${node.name} 标签,可能导致无法播放`, node);
|
||||
this.bubble();
|
||||
break;
|
||||
case 'td':
|
||||
case 'th':
|
||||
if (attrs.colspan || attrs.rowspan)
|
||||
for (var k = this.STACK.length, item; item = this.STACK[--k];)
|
||||
if (item.name == 'table') {
|
||||
item.c = void 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (attrs.align) {
|
||||
styleObj['text-align'] = attrs.align;
|
||||
attrs.align = void 0;
|
||||
}
|
||||
// 压缩 style
|
||||
var styles = style.split(';');
|
||||
style = '';
|
||||
for (var i = 0, len = styles.length; i < len; i++) {
|
||||
var info = styles[i].split(':');
|
||||
if (info.length < 2) continue;
|
||||
let key = info[0].trim().toLowerCase(),
|
||||
value = info.slice(1).join(':').trim();
|
||||
if (value.includes('-webkit') || value.includes('-moz') || value.includes('-ms') || value.includes('-o') || value
|
||||
.includes(
|
||||
'safe'))
|
||||
style += `;${key}:${value}`;
|
||||
else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import'))
|
||||
styleObj[key] = value;
|
||||
}
|
||||
if (node.name == 'img') {
|
||||
if (attrs['data-src']) {
|
||||
attrs.src = attrs.src || attrs['data-src'];
|
||||
attrs['data-src'] = void 0;
|
||||
}
|
||||
if (attrs.src && !attrs.ignore) {
|
||||
if (this.bubble())
|
||||
attrs.i = (this.imgNum++).toString();
|
||||
else attrs.ignore = 'T';
|
||||
}
|
||||
if (attrs.ignore) styleObj['max-width'] = '100%';
|
||||
var width;
|
||||
if (styleObj.width) width = styleObj.width;
|
||||
else if (attrs.width) width = attrs.width.includes('%') ? attrs.width : attrs.width + 'px';
|
||||
if (width) {
|
||||
styleObj.width = width;
|
||||
attrs.width = '100%';
|
||||
if (parseInt(width) > windowWidth) {
|
||||
styleObj.height = '';
|
||||
if (attrs.height) attrs.height = void 0;
|
||||
}
|
||||
}
|
||||
if (styleObj.height) {
|
||||
attrs.height = styleObj.height;
|
||||
styleObj.height = '';
|
||||
} else if (attrs.height && !attrs.height.includes('%'))
|
||||
attrs.height += 'px';
|
||||
}
|
||||
for (var key in styleObj) {
|
||||
var value = styleObj[key];
|
||||
if (key.includes('flex') || key == 'order' || key == 'self-align') node.c = 1;
|
||||
// 填充链接
|
||||
if (value.includes('url')) {
|
||||
var j = value.indexOf('(');
|
||||
if (j++ != -1) {
|
||||
while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++;
|
||||
value = value.substr(0, j) + this.getUrl(value.substr(j));
|
||||
}
|
||||
}
|
||||
// 转换 rpx
|
||||
else if (value.includes('rpx'))
|
||||
value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px');
|
||||
else if (key == 'white-space' && value.includes('pre'))
|
||||
this.pre = node.pre = true;
|
||||
style += `;${key}:${value}`;
|
||||
}
|
||||
style = style.substr(1);
|
||||
if (style) attrs.style = style;
|
||||
}
|
||||
// 节点出栈处理
|
||||
popNode(node) {
|
||||
// 空白符处理
|
||||
if (node.pre) {
|
||||
node.pre = this.pre = void 0;
|
||||
for (let i = this.STACK.length; i--;)
|
||||
if (this.STACK[i].pre)
|
||||
this.pre = true;
|
||||
}
|
||||
var siblings = this.siblings(),
|
||||
len = siblings.length,
|
||||
childs = node.children;
|
||||
if (node.name == 'head' || (cfg.filter && cfg.filter(node, this) == false))
|
||||
return siblings.pop();
|
||||
var attrs = node.attrs;
|
||||
// 替换一些标签名
|
||||
if (cfg.blockTags[node.name]) node.name = 'div';
|
||||
else if (!cfg.trustTags[node.name]) node.name = 'span';
|
||||
// 去除块标签前后空串
|
||||
if (node.name == 'div' || node.name == 'p' || node.name[0] == 't') {
|
||||
if (len > 1 && siblings[len - 2].text == ' ')
|
||||
siblings.splice(--len - 1, 1);
|
||||
if (childs.length && childs[childs.length - 1].text == ' ')
|
||||
childs.pop();
|
||||
}
|
||||
// 处理列表
|
||||
if (node.c && (node.name == 'ul' || node.name == 'ol')) {
|
||||
if ((node.attrs.style || '').includes('list-style:none')) {
|
||||
for (let i = 0, child; child = childs[i++];)
|
||||
if (child.name == 'li')
|
||||
child.name = 'div';
|
||||
} else if (node.name == 'ul') {
|
||||
var floor = 1;
|
||||
for (let i = this.STACK.length; i--;)
|
||||
if (this.STACK[i].name == 'ul') floor++;
|
||||
if (floor != 1)
|
||||
for (let i = childs.length; i--;)
|
||||
childs[i].floor = floor;
|
||||
} else {
|
||||
for (let i = 0, num = 1, child; child = childs[i++];)
|
||||
if (child.name == 'li') {
|
||||
child.type = 'ol';
|
||||
child.num = ((num, type) => {
|
||||
if (type == 'a') return String.fromCharCode(97 + (num - 1) % 26);
|
||||
if (type == 'A') return String.fromCharCode(65 + (num - 1) % 26);
|
||||
if (type == 'i' || type == 'I') {
|
||||
num = (num - 1) % 99 + 1;
|
||||
var one = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'],
|
||||
ten = ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'],
|
||||
res = (ten[Math.floor(num / 10) - 1] || '') + (one[num % 10 - 1] || '');
|
||||
if (type == 'i') return res.toLowerCase();
|
||||
return res;
|
||||
}
|
||||
return num;
|
||||
})(num++, attrs.type) + '.';
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理表格的边框
|
||||
if (node.name == 'table') {
|
||||
var padding = attrs.cellpadding,
|
||||
spacing = attrs.cellspacing,
|
||||
border = attrs.border;
|
||||
if (node.c) {
|
||||
this.bubble();
|
||||
if (!padding) padding = 2;
|
||||
if (!spacing) spacing = 2;
|
||||
}
|
||||
if (border) attrs.style = `border:${border}px solid gray;${attrs.style || ''}`;
|
||||
if (spacing) attrs.style = `border-spacing:${spacing}px;${attrs.style || ''}`;
|
||||
if (border || padding)
|
||||
(function f(ns) {
|
||||
for (var i = 0, n; n = ns[i]; i++) {
|
||||
if (n.name == 'th' || n.name == 'td') {
|
||||
if (border) n.attrs.style = `border:${border}px solid gray;${n.attrs.style}`;
|
||||
if (padding) n.attrs.style = `padding:${padding}px;${n.attrs.style}`;
|
||||
} else f(n.children || []);
|
||||
}
|
||||
})(childs)
|
||||
if (this.options.autoscroll) {
|
||||
var table = Object.assign({}, node);
|
||||
node.name = 'div';
|
||||
node.attrs = {
|
||||
style: 'overflow:scroll'
|
||||
}
|
||||
node.children = [table];
|
||||
}
|
||||
}
|
||||
this.CssHandler.pop && this.CssHandler.pop(node);
|
||||
// 自动压缩
|
||||
if (node.name == 'div' && !Object.keys(attrs).length && childs.length == 1 && childs[0].name == 'div')
|
||||
siblings[len - 1] = childs[0];
|
||||
}
|
||||
// 工具函数
|
||||
bubble() {
|
||||
for (var i = this.STACK.length, item; item = this.STACK[--i];) {
|
||||
if (cfg.richOnlyTags[item.name]) {
|
||||
if (item.name == 'table' && !Object.hasOwnProperty.call(item, 'c')) item.c = 1;
|
||||
return false;
|
||||
}
|
||||
item.c = 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
decode(val, amp) {
|
||||
var i = -1,
|
||||
j, en;
|
||||
while (1) {
|
||||
if ((i = val.indexOf('&', i + 1)) == -1) break;
|
||||
if ((j = val.indexOf(';', i + 2)) == -1) break;
|
||||
if (val[i + 1] == '#') {
|
||||
en = parseInt((val[i + 2] == 'x' ? '0' : '') + val.substring(i + 2, j));
|
||||
if (!isNaN(en)) val = val.substr(0, i) + String.fromCharCode(en) + val.substr(j + 1);
|
||||
} else {
|
||||
en = val.substring(i + 1, j);
|
||||
if (cfg.entities[en] || en == amp)
|
||||
val = val.substr(0, i) + (cfg.entities[en] || '&') + val.substr(j + 1);
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
getUrl(url) {
|
||||
if (url[0] == '/') {
|
||||
if (url[1] == '/') url = this.options.prot + ':' + url;
|
||||
else if (this.domain) url = this.domain + url;
|
||||
} else if (this.domain && url.indexOf('data:') != 0 && !url.includes('://'))
|
||||
url = this.domain + '/' + url;
|
||||
return url;
|
||||
}
|
||||
isClose() {
|
||||
return this.data[this.i] == '>' || (this.data[this.i] == '/' && this.data[this.i + 1] == '>');
|
||||
}
|
||||
section() {
|
||||
return this.data.substring(this.start, this.i);
|
||||
}
|
||||
parent() {
|
||||
return this.STACK[this.STACK.length - 1];
|
||||
}
|
||||
siblings() {
|
||||
return this.STACK.length ? this.parent().children : this.DOM;
|
||||
}
|
||||
// 状态机
|
||||
Text(c) {
|
||||
if (c == '<') {
|
||||
var next = this.data[this.i + 1],
|
||||
isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||||
if (isLetter(next)) {
|
||||
this.setText();
|
||||
this.start = this.i + 1;
|
||||
this.state = this.TagName;
|
||||
} else if (next == '/') {
|
||||
this.setText();
|
||||
if (isLetter(this.data[++this.i + 1])) {
|
||||
this.start = this.i + 1;
|
||||
this.state = this.EndTag;
|
||||
} else
|
||||
this.Comment();
|
||||
} else if (next == '!') {
|
||||
this.setText();
|
||||
this.Comment();
|
||||
}
|
||||
}
|
||||
}
|
||||
Comment() {
|
||||
var key;
|
||||
if (this.data.substring(this.i + 2, this.i + 4) == '--') key = '-->';
|
||||
else if (this.data.substring(this.i + 2, this.i + 9) == '[CDATA[') key = ']]>';
|
||||
else key = '>';
|
||||
if ((this.i = this.data.indexOf(key, this.i + 2)) == -1) this.i = this.data.length;
|
||||
else this.i += key.length - 1;
|
||||
this.start = this.i + 1;
|
||||
this.state = this.Text;
|
||||
}
|
||||
TagName(c) {
|
||||
if (blankChar[c]) {
|
||||
this.tagName = this.section();
|
||||
while (blankChar[this.data[this.i]]) this.i++;
|
||||
if (this.isClose()) this.setNode();
|
||||
else {
|
||||
this.start = this.i;
|
||||
this.state = this.AttrName;
|
||||
}
|
||||
} else if (this.isClose()) {
|
||||
this.tagName = this.section();
|
||||
this.setNode();
|
||||
}
|
||||
}
|
||||
AttrName(c) {
|
||||
var blank = blankChar[c];
|
||||
if (blank) {
|
||||
this.attrName = this.section();
|
||||
c = this.data[this.i];
|
||||
}
|
||||
if (c == '=') {
|
||||
if (!blank) this.attrName = this.section();
|
||||
while (blankChar[this.data[++this.i]]);
|
||||
this.start = this.i--;
|
||||
this.state = this.AttrValue;
|
||||
} else if (blank) this.setAttr();
|
||||
else if (this.isClose()) {
|
||||
this.attrName = this.section();
|
||||
this.setAttr();
|
||||
}
|
||||
}
|
||||
AttrValue(c) {
|
||||
if (c == '"' || c == "'") {
|
||||
this.start++;
|
||||
if ((this.i = this.data.indexOf(c, this.i + 1)) == -1) return this.i = this.data.length;
|
||||
this.attrVal = this.section();
|
||||
this.i++;
|
||||
} else {
|
||||
for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++);
|
||||
this.attrVal = this.section();
|
||||
}
|
||||
this.setAttr();
|
||||
}
|
||||
EndTag(c) {
|
||||
if (blankChar[c] || c == '>' || c == '/') {
|
||||
var name = this.section().toLowerCase();
|
||||
for (var i = this.STACK.length; i--;)
|
||||
if (this.STACK[i].name == name) break;
|
||||
if (i != -1) {
|
||||
var node;
|
||||
while ((node = this.STACK.pop()).name != name);
|
||||
this.popNode(node);
|
||||
} else if (name == 'p' || name == 'br')
|
||||
this.siblings().push({
|
||||
name,
|
||||
attrs: {}
|
||||
});
|
||||
this.i = this.data.indexOf('>', this.i);
|
||||
this.start = this.i + 1;
|
||||
if (this.i == -1) this.i = this.data.length;
|
||||
else this.state = this.Text;
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = MpHtmlParser;
|
||||
98
components/jyf-parser/libs/config.js
Normal file
98
components/jyf-parser/libs/config.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/* 配置文件 */
|
||||
// #ifdef MP-WEIXIN
|
||||
const canIUse = wx.canIUse('editor'); // 高基础库标识,用于兼容
|
||||
// #endif
|
||||
module.exports = {
|
||||
// ivew
|
||||
transpileDependencies: ['uview-ui'],
|
||||
// 过滤器函数
|
||||
filter: null,
|
||||
// 代码高亮函数
|
||||
highlight: null,
|
||||
// 文本处理函数
|
||||
onText: null,
|
||||
// 实体编码列表
|
||||
entities: {
|
||||
quot: '"',
|
||||
apos: "'",
|
||||
semi: ';',
|
||||
nbsp: '\xA0',
|
||||
ensp: '\u2002',
|
||||
emsp: '\u2003',
|
||||
ndash: '–',
|
||||
mdash: '—',
|
||||
middot: '·',
|
||||
lsquo: '‘',
|
||||
rsquo: '’',
|
||||
ldquo: '“',
|
||||
rdquo: '”',
|
||||
bull: '•',
|
||||
hellip: '…'
|
||||
},
|
||||
blankChar: makeMap(' ,\xA0,\t,\r,\n,\f'),
|
||||
// 块级标签,将被转为 div
|
||||
blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,section' + (
|
||||
// #ifdef MP-WEIXIN
|
||||
canIUse ? '' :
|
||||
// #endif
|
||||
',pre')),
|
||||
// 将被移除的标签
|
||||
ignoreTags: makeMap(
|
||||
'area,base,basefont,canvas,command,frame,input,isindex,keygen,link,map,meta,param,script,source,style,svg,textarea,title,track,use,wbr'
|
||||
// #ifdef MP-WEIXIN
|
||||
+ (canIUse ? ',rp' : '')
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
+ ',embed,iframe'
|
||||
// #endif
|
||||
),
|
||||
// 只能被 rich-text 显示的标签
|
||||
richOnlyTags: makeMap('a,colgroup,fieldset,legend,picture,table'
|
||||
// #ifdef MP-WEIXIN
|
||||
+ (canIUse ? ',bdi,bdo,caption,rt,ruby' : '')
|
||||
// #endif
|
||||
),
|
||||
// 自闭合的标签
|
||||
selfClosingTags: makeMap(
|
||||
'area,base,basefont,br,col,circle,ellipse,embed,frame,hr,img,input,isindex,keygen,line,link,meta,param,path,polygon,rect,source,track,use,wbr'
|
||||
),
|
||||
// 信任的属性
|
||||
trustAttrs: makeMap(
|
||||
'align,alt,app-id,author,autoplay,border,cellpadding,cellspacing,class,color,colspan,controls,data-src,dir,face,height,href,id,ignore,loop,media,muted,name,path,poster,rowspan,size,span,src,start,style,type,unit-id,width,xmlns'
|
||||
),
|
||||
// bool 型的属性
|
||||
boolAttrs: makeMap('autoplay,controls,ignore,loop,muted'),
|
||||
// 信任的标签
|
||||
trustTags: makeMap(
|
||||
'a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'
|
||||
// #ifdef MP-WEIXIN
|
||||
+ (canIUse ? ',bdi,bdo,caption,pre,rt,ruby' : '')
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
+ ',embed,iframe'
|
||||
// #endif
|
||||
),
|
||||
// 默认的标签样式
|
||||
userAgentStyles: {
|
||||
address: 'font-style:italic',
|
||||
big: 'display:inline;font-size:1.2em',
|
||||
blockquote: 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px',
|
||||
caption: 'display:table-caption;text-align:center',
|
||||
center: 'text-align:center',
|
||||
cite: 'font-style:italic',
|
||||
dd: 'margin-left:40px',
|
||||
mark: 'background-color:yellow',
|
||||
pre: 'font-family:monospace;white-space:pre;overflow:scroll',
|
||||
s: 'text-decoration:line-through',
|
||||
small: 'display:inline;font-size:0.8em',
|
||||
u: 'text-decoration:underline'
|
||||
}
|
||||
}
|
||||
|
||||
function makeMap(str) {
|
||||
var map = {},
|
||||
list = str.split(',');
|
||||
for (var i = list.length; i--;)
|
||||
map[list[i]] = true;
|
||||
return map;
|
||||
}
|
||||
20
components/jyf-parser/libs/handler.sjs
Normal file
20
components/jyf-parser/libs/handler.sjs
Normal file
@@ -0,0 +1,20 @@
|
||||
var inlineTags = {
|
||||
abbr: 1,
|
||||
b: 1,
|
||||
big: 1,
|
||||
code: 1,
|
||||
del: 1,
|
||||
em: 1,
|
||||
i: 1,
|
||||
ins: 1,
|
||||
label: 1,
|
||||
q: 1,
|
||||
small: 1,
|
||||
span: 1,
|
||||
strong: 1
|
||||
}
|
||||
export default {
|
||||
useRichText: function(item) {
|
||||
return !item.c && !inlineTags[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1;
|
||||
}
|
||||
}
|
||||
20
components/jyf-parser/libs/handler.wxs
Normal file
20
components/jyf-parser/libs/handler.wxs
Normal file
@@ -0,0 +1,20 @@
|
||||
var inlineTags = {
|
||||
abbr: 1,
|
||||
b: 1,
|
||||
big: 1,
|
||||
code: 1,
|
||||
del: 1,
|
||||
em: 1,
|
||||
i: 1,
|
||||
ins: 1,
|
||||
label: 1,
|
||||
q: 1,
|
||||
small: 1,
|
||||
span: 1,
|
||||
strong: 1
|
||||
}
|
||||
module.exports = {
|
||||
useRichText: function(item) {
|
||||
return !item.c && !inlineTags[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1;
|
||||
}
|
||||
}
|
||||
519
components/jyf-parser/libs/trees.vue
Normal file
519
components/jyf-parser/libs/trees.vue
Normal file
@@ -0,0 +1,519 @@
|
||||
<template>
|
||||
<view class="interlayer">
|
||||
<block v-for="(n, index) in ns" v-bind:key="index">
|
||||
<!--图片-->
|
||||
<view v-if="n.name=='img'" :class="'_img '+n.attrs.class" :style="n.attrs.style" :data-attrs="n.attrs" @tap="imgtap">
|
||||
<rich-text :nodes="[{attrs:{src:lazyLoad&&!n.load?placeholder:n.attrs.src,alt:n.attrs.alt||'',width:n.attrs.width||'',style:'max-width:100%;display:block'+(n.attrs.height?';height:'+n.attrs.height:'')},name:'img'}]" />
|
||||
<image class="_image" :src="lazyLoad&&!n.load?placeholder:n.attrs.src" :lazy-load="lazyLoad"
|
||||
:show-menu-by-longpress="!n.attrs.ignore" :data-i="index" data-source="img" @load="loadImg" @error="error" />
|
||||
</view>
|
||||
<!--文本-->
|
||||
<text v-else-if="n.type=='text'" decode>{{n.text}}</text>
|
||||
<!--#ifndef MP-BAIDU-->
|
||||
<text v-else-if="n.name=='br'">\n</text>
|
||||
<!--#endif-->
|
||||
<!--视频-->
|
||||
<view v-else-if="n.lazyLoad||(n.name=='video'&&!loadVideo)" :id="n.attrs.id" :class="'_video '+(n.attrs.class||'')"
|
||||
:style="n.attrs.style" :data-i="index" @tap="_loadVideo" />
|
||||
<video v-else-if="n.name=='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay"
|
||||
:controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.attrs.source[n.i||0]"
|
||||
:unit-id="n.attrs['unit-id']" :data-id="n.attrs.id" :data-i="index" data-source="video" @error="error" @play="play" />
|
||||
<!--音频-->
|
||||
<audio v-else-if="n.name=='audio'" :ref="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author"
|
||||
:autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster"
|
||||
:src="n.attrs.source[n.i||0]" :data-i="index" :data-id="n.attrs.id" data-source="audio" @error.native="error"
|
||||
@play.native="play" />
|
||||
<!--链接-->
|
||||
<view v-else-if="n.name=='a'" :class="'_a '+(n.attrs.class||'')" hover-class="_hover" :style="n.attrs.style"
|
||||
:data-attrs="n.attrs" @tap="linkpress">
|
||||
<trees class="_span" :nodes="n.children" />
|
||||
</view>
|
||||
<!--广告(按需打开注释)-->
|
||||
<!--#ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO-->
|
||||
<!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :unit-id="n.attrs['unit-id']"
|
||||
data-from="ad" @error="error" />-->
|
||||
<!--#endif-->
|
||||
<!--#ifdef MP-BAIDU-->
|
||||
<!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :appid="n.attrs.appid"
|
||||
:apid="n.attrs.apid" :type="n.attrs.type" data-from="ad" @error="error" />-->
|
||||
<!--#endif-->
|
||||
<!--#ifdef APP-PLUS-->
|
||||
<!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :adpid="n.attrs.adpid"
|
||||
data-from="ad" @error="error" />-->
|
||||
<!--#endif-->
|
||||
<!--列表-->
|
||||
<view v-else-if="n.name=='li'" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:flex'">
|
||||
<view v-if="n.type=='ol'" class="_ol-bef">{{n.num}}</view>
|
||||
<view v-else class="_ul-bef">
|
||||
<view v-if="n.floor%3==0" class="_ul-p1">█</view>
|
||||
<view v-else-if="n.floor%3==2" class="_ul-p2" />
|
||||
<view v-else class="_ul-p1" style="border-radius:50%">█</view>
|
||||
</view>
|
||||
<!--#ifdef MP-ALIPAY-->
|
||||
<view class="_li">
|
||||
<trees :nodes="n.children" :lazyLoad="lazyLoad" />
|
||||
</view>
|
||||
<!--#endif-->
|
||||
<!--#ifndef MP-ALIPAY-->
|
||||
<trees class="_li" :nodes="n.children" :lazyLoad="lazyLoad" />
|
||||
<!--#endif-->
|
||||
</view>
|
||||
<!--表格-->
|
||||
<view v-else-if="n.name=='table'&&n.c" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:table'">
|
||||
<view v-for="(tbody, i) in n.children" v-bind:key="i" :class="tbody.attrs.class" :style="(tbody.attrs.style||'')+(tbody.name[0]=='t'?';display:table-'+(tbody.name=='tr'?'row':'row-group'):'')">
|
||||
<view v-for="(tr, j) in tbody.children" v-bind:key="j" :class="tr.attrs.class" :style="(tr.attrs.style||'')+(tr.name[0]=='t'?';display:table-'+(tr.name=='tr'?'row':'cell'):'')">
|
||||
<trees v-if="tr.name=='td'" :nodes="tr.children" />
|
||||
<block v-else>
|
||||
<!--#ifdef MP-ALIPAY-->
|
||||
<view v-for="(td, k) in tr.children" v-bind:key="k" :class="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')">
|
||||
<trees :nodes="td.children" />
|
||||
</view>
|
||||
<!--#endif-->
|
||||
<!--#ifndef MP-ALIPAY-->
|
||||
<trees v-for="(td, k) in tr.children" v-bind:key="k" :class="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')"
|
||||
:nodes="td.children" />
|
||||
<!--#endif-->
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!--#ifdef APP-PLUS-->
|
||||
<iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder"
|
||||
:width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
|
||||
<embed v-else-if="n.name=='embed'" :style="n.attrs.style" :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
|
||||
<!--#endif-->
|
||||
<!--富文本-->
|
||||
<!--#ifdef MP-WEIXIN || MP-QQ || MP-ALIPAY || APP-PLUS-->
|
||||
<rich-text v-else-if="handler.useRichText(n)" :id="n.attrs.id" :class="'_p __'+n.name" :nodes="[n]" />
|
||||
<!--#endif-->
|
||||
<!--#ifdef MP-BAIDU || MP-TOUTIAO-->
|
||||
<rich-text v-else-if="!n.c" :id="n.attrs.id" :nodes="[n]" />
|
||||
<!--#endif-->
|
||||
<!--#ifdef MP-ALIPAY-->
|
||||
<view v-else :id="n.attrs.id" :class="'_'+n.name+' '+(n.attrs.class||'')" :style="n.attrs.style">
|
||||
<trees :nodes="n.children" :lazyLoad="lazyLoad" />
|
||||
</view>
|
||||
<!--#endif-->
|
||||
<!--#ifndef MP-ALIPAY-->
|
||||
<trees v-else :class="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')" :style="n.attrs.style" :nodes="n.children"
|
||||
:lazyLoad="lazyLoad" />
|
||||
<!--#endif-->
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
<script module="handler" lang="wxs" src="./handler.wxs"></script>
|
||||
<script module="handler" lang="sjs" src="./handler.sjs"></script>
|
||||
<script>
|
||||
global.Parser = {};
|
||||
import trees from './trees'
|
||||
export default {
|
||||
components: {
|
||||
trees
|
||||
},
|
||||
name: 'trees',
|
||||
data() {
|
||||
return {
|
||||
ns: [],
|
||||
placeholder: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="225"/>',
|
||||
loadVideo:
|
||||
// #ifdef APP-PLUS
|
||||
false
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
true
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
props: {
|
||||
nodes: Array,
|
||||
lazyLoad: Boolean,
|
||||
},
|
||||
watch: {
|
||||
nodes: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.ns = val;
|
||||
// #ifdef APP-PLUS
|
||||
// APP 上避免 video 错位需要延时渲染
|
||||
setTimeout(() => {
|
||||
this.loadVideo = true;
|
||||
}, 3000)
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 获取顶层组件
|
||||
var _top = this.$parent;
|
||||
while (_top.$options.name != 'parser') {
|
||||
if (_top.top) {
|
||||
_top = _top.top;
|
||||
break;
|
||||
}
|
||||
_top = _top.$parent;
|
||||
}
|
||||
this.top = _top;
|
||||
for (var j = this.nodes.length, item; item = this.nodes[--j];) {
|
||||
if (item.c) continue;
|
||||
if (item.name == 'img')
|
||||
_top.imgList.setItem(item.attrs.i, item.attrs.src);
|
||||
else if (item.name == 'video' || item.name == 'audio') {
|
||||
var ctx;
|
||||
if (item.name == 'video')
|
||||
ctx = uni.createVideoContext(item.attrs.id
|
||||
// #ifndef MP-BAIDU
|
||||
, this
|
||||
// #endif
|
||||
);
|
||||
else if (this.$refs[item.attrs.id])
|
||||
ctx = this.$refs[item.attrs.id][0];
|
||||
if (ctx) {
|
||||
ctx.id = item.attrs.id;
|
||||
_top.videoContexts.push(ctx);
|
||||
}
|
||||
}
|
||||
// #ifdef MP-BAIDU || MP-ALIPAY || APP-PLUS
|
||||
if (item.attrs && item.attrs.id) {
|
||||
_top.anchors = _top.anchors || [];
|
||||
_top.anchors.push({
|
||||
id: item.attrs.id,
|
||||
node: this
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
play(e) {
|
||||
var contexts = this.top.videoContexts;
|
||||
if (contexts.length > 1 && this.top.autopause)
|
||||
for (var i = contexts.length; i--;)
|
||||
if (contexts[i].id != e.currentTarget.dataset.id)
|
||||
contexts[i].pause();
|
||||
},
|
||||
imgtap(e) {
|
||||
var attrs = e.currentTarget.dataset.attrs;
|
||||
if (!attrs.ignore) {
|
||||
var preview = true,
|
||||
data = {
|
||||
id: e.target.id,
|
||||
src: attrs.src,
|
||||
ignore: () => preview = false
|
||||
};
|
||||
global.Parser.onImgtap && global.Parser.onImgtap(data);
|
||||
this.top.$emit('imgtap', data);
|
||||
if (preview) {
|
||||
var urls = this.top.imgList,
|
||||
current = urls[attrs.i] ? parseInt(attrs.i) : (urls = [attrs.src], 0);
|
||||
uni.previewImage({
|
||||
current,
|
||||
urls
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
loadImg(e) {
|
||||
var node = this.ns[e.currentTarget.dataset.i];
|
||||
if (this.lazyLoad && !node.load)
|
||||
this.$set(node, 'load', true);
|
||||
},
|
||||
linkpress(e) {
|
||||
var jump = true,
|
||||
attrs = e.currentTarget.dataset.attrs;
|
||||
attrs.ignore = () => jump = false;
|
||||
global.Parser.onLinkpress && global.Parser.onLinkpress(attrs);
|
||||
this.top.$emit('linkpress', attrs);
|
||||
if (jump) {
|
||||
// #ifdef MP
|
||||
if (attrs['app-id']) {
|
||||
return uni.navigateToMiniProgram({
|
||||
appId: attrs['app-id'],
|
||||
path: attrs.path
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
if (attrs.href) {
|
||||
if (attrs.href[0] == '#') {
|
||||
if (this.top.useAnchor)
|
||||
this.top.navigateTo({
|
||||
id: attrs.href.substring(1)
|
||||
})
|
||||
} else if (attrs.href.indexOf('http') == 0 || attrs.href.indexOf('//') == 0) {
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openWeb(attrs.href);
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
uni.setClipboardData({
|
||||
data: attrs.href,
|
||||
success: () =>
|
||||
uni.showToast({
|
||||
title: '链接已复制'
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
} else
|
||||
uni.navigateTo({
|
||||
url: attrs.href,
|
||||
fail() {
|
||||
uni.switchTab({
|
||||
url: attrs.href,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
error(e) {
|
||||
var context, src = '',
|
||||
target = e.currentTarget,
|
||||
source = target.dataset.source,
|
||||
node = this.ns[target.dataset.i];
|
||||
if (source == 'video' || source == 'audio') {
|
||||
// 加载其他 source
|
||||
var index = (node.i || 0) + 1;
|
||||
if (index < node.attrs.source.length)
|
||||
this.$set(node, 'i', index);
|
||||
if (source == 'video') context = uni.createVideoContext(target.id, this);
|
||||
else if (e.detail.__args__) {
|
||||
e.detail = e.detail.__args__[0];
|
||||
context = e.detail.context;
|
||||
}
|
||||
} else if (source == 'img')
|
||||
context = {
|
||||
setSrc: src => {
|
||||
node.attrs.src = src;
|
||||
}
|
||||
}
|
||||
this.top && this.top.$emit('error', {
|
||||
source,
|
||||
target,
|
||||
errMsg: e.detail.errMsg,
|
||||
errCode: e.detail.errCode,
|
||||
context
|
||||
});
|
||||
},
|
||||
_loadVideo(e) {
|
||||
var i = e.target.dataset.i;
|
||||
this.ns[i].lazyLoad = false;
|
||||
this.ns[i].attrs.autoplay = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 在这里引入自定义样式 */
|
||||
|
||||
/* 链接和图片效果 */
|
||||
._a {
|
||||
display: inline;
|
||||
padding: 1.5px 0 1.5px 0;
|
||||
color: #366092;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
._hover {
|
||||
text-decoration: underline;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
._img {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* #ifdef MP-WEIXIN */
|
||||
:host {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef MP */
|
||||
.interlayer {
|
||||
display: inherit;
|
||||
flex-direction: inherit;
|
||||
flex-wrap: inherit;
|
||||
align-content: inherit;
|
||||
align-items: inherit;
|
||||
justify-content: inherit;
|
||||
width: 100%;
|
||||
white-space: inherit;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
._b,
|
||||
._strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._blockquote,
|
||||
._div,
|
||||
._p,
|
||||
._ol,
|
||||
._ul,
|
||||
._li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
._code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
._del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
._em,
|
||||
._i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
._h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
._h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
._h3 {
|
||||
font-size: 1.17em;
|
||||
}
|
||||
|
||||
._h5 {
|
||||
font-size: 0.83em;
|
||||
}
|
||||
|
||||
._h6 {
|
||||
font-size: 0.67em;
|
||||
}
|
||||
|
||||
._h1,
|
||||
._h2,
|
||||
._h3,
|
||||
._h4,
|
||||
._h5,
|
||||
._h6 {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
._ins {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
._li {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
._ol-bef {
|
||||
width: 36px;
|
||||
margin-right: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
._ul-bef {
|
||||
margin: 0 12px 0 23px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
._ol-bef,
|
||||
._ul_bef {
|
||||
flex: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
._ul-p1 {
|
||||
display: inline-block;
|
||||
width: 0.3em;
|
||||
height: 0.3em;
|
||||
overflow: hidden;
|
||||
line-height: 0.3em;
|
||||
}
|
||||
|
||||
._ul-p2 {
|
||||
display: inline-block;
|
||||
width: 0.23em;
|
||||
height: 0.23em;
|
||||
border: 0.05em solid black;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
._q::before {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._q::after {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._sub {
|
||||
font-size: smaller;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
._sup {
|
||||
font-size: smaller;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
/* #ifdef MP-ALIPAY || APP-PLUS */
|
||||
._abbr,
|
||||
._b,
|
||||
._code,
|
||||
._del,
|
||||
._em,
|
||||
._i,
|
||||
._ins,
|
||||
._label,
|
||||
._q,
|
||||
._span,
|
||||
._strong,
|
||||
._sub,
|
||||
._sup {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef MP-WEIXIN || MP-QQ */
|
||||
.__bdo,
|
||||
.__bdi,
|
||||
.__ruby,
|
||||
.__rt {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
._video {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 300px;
|
||||
height: 225px;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
._video::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -15px 0 0 -15px;
|
||||
content: '';
|
||||
border-color: transparent transparent transparent white;
|
||||
border-style: solid;
|
||||
border-width: 15px 0 15px 30px;
|
||||
}
|
||||
</style>
|
||||
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;
|
||||
116
components/navigation-video/navigation-video.vue
Normal file
116
components/navigation-video/navigation-video.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<view class="footer_box" :class="{'footer_bg': bg}" :style="{paddingBottom: bottomBlackLineHeight + 'rpx'}">
|
||||
<view class="footer_item" v-for="(item, index) of navigationList" :key="index" @click="onPageJump(item)">
|
||||
<text class="footer_item_text" :class="[item.pagePath == path ? 'footer_item_text_active' : '']">{{ item.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import $http from '@/config/requestConfig';
|
||||
import {
|
||||
judgeLogin
|
||||
} from '@/config/login.js';
|
||||
export default {
|
||||
props: {
|
||||
bg: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
bottomBlackLineHeight: {
|
||||
type: Number,
|
||||
default: function(){
|
||||
return 0
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
path: '',
|
||||
navigationList: [
|
||||
{
|
||||
text: '首页',
|
||||
pagePath: 'pages/peanut/home',
|
||||
tab: true
|
||||
},
|
||||
{
|
||||
text: '购物车',
|
||||
pagePath: 'pages/peanut/shopping',
|
||||
tab: true
|
||||
},
|
||||
{
|
||||
text: '我的书架',
|
||||
pagePath: 'pages/peanut/bookshelf',
|
||||
tab: true
|
||||
},
|
||||
{
|
||||
text: '我的',
|
||||
pagePath: 'pages/peanut/mine',
|
||||
tab: true
|
||||
},
|
||||
]
|
||||
};
|
||||
},
|
||||
//第一次加载
|
||||
created() {
|
||||
//获取所有页面
|
||||
let currentPages = getCurrentPages();
|
||||
let page = currentPages[currentPages.length - 1];
|
||||
this.path = page.route;
|
||||
},
|
||||
//方法
|
||||
methods: {
|
||||
onPageJump(item) {
|
||||
if(item.tab){
|
||||
uni.switchTab({
|
||||
url: "/" + item.pagePath
|
||||
});
|
||||
} else {
|
||||
uni.reLaunch({
|
||||
url: "/" + item.pagePath
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/style/mixin.scss';
|
||||
.footer_station {
|
||||
height: 100rpx;
|
||||
}
|
||||
.footer_box {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 750rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
z-index: 10;
|
||||
}
|
||||
.footer_bg {
|
||||
// background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
.footer_item {
|
||||
flex: 1;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 98rpx;
|
||||
}
|
||||
.footer_item:active {
|
||||
background-color: rgba($color: #fff, $alpha: 0.1);
|
||||
}
|
||||
.footer_item_text {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
.footer_item_text_active {
|
||||
color: #ffffff;
|
||||
}
|
||||
</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>
|
||||
214
components/short-video/short-video.vue
Normal file
214
components/short-video/short-video.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<view class="video_box" :style="{ height: screenHeight + 'px'}">
|
||||
<video
|
||||
class="video_file"
|
||||
:id="`video_${objId}`"
|
||||
:ref="`video_${objId}`"
|
||||
:src="src"
|
||||
:poster="poster"
|
||||
loop
|
||||
object-fit="contain"
|
||||
:show-play-btn="false"
|
||||
:show-center-play-btn="false"
|
||||
:controls="false"
|
||||
:enable-progress-gesture="false"
|
||||
:autoplay="videoPlayId == objId"
|
||||
:style="'height: '+screenHeight + 'px'"
|
||||
@timeupdate="onScheduleChange"
|
||||
@waiting="onWaiting"
|
||||
@error="onError"
|
||||
></video>
|
||||
<!-- <view class="bottom_mask"></view> -->
|
||||
<!-- 播放按钮 -->
|
||||
<view class="play_btn" @click="onPlay">
|
||||
<image class="icon_play" v-if="playState == 1000" src="http://qn.kemean.cn/upload/202103/08/1615183536616rn7yfe5g.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="play_schedule" :style="{bottom: progressBottom + 'rpx'}">
|
||||
<view class="schedule_bg"></view>
|
||||
<view class="schedule" :style="{ width: schedule + 'rpx' }"></view>
|
||||
<view class="progress_drag_dot" v-if="progressDrag"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex';
|
||||
export default {
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
objId: {
|
||||
default: ""
|
||||
},
|
||||
poster: {
|
||||
//视频封面的图片
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
screenHeight: {
|
||||
type: Number,
|
||||
default: function(){
|
||||
return 1334
|
||||
}
|
||||
},
|
||||
progressBottom: {
|
||||
type: Number,
|
||||
default: function(){
|
||||
return 88
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
schedule: 0,
|
||||
// 1000 待播放
|
||||
// 2000 播放中
|
||||
// 3000 播放缓冲中
|
||||
playState: 1000,
|
||||
// 是否拖动进度
|
||||
progressDrag: false,
|
||||
// 视频总长度
|
||||
duration: 0,
|
||||
videoCtx: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['videoPlayId'])
|
||||
},
|
||||
watch: {
|
||||
videoPlayId(val) {
|
||||
if (val == this.objId) {
|
||||
setTimeout(() => {
|
||||
this.videoCtx.play();
|
||||
this.playState = 2000;
|
||||
}, 200);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.videoCtx.pause();
|
||||
this.videoCtx.seek(0);
|
||||
this.playState = 1000;
|
||||
this.schedule = 0;
|
||||
},100);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.videoCtx = uni.createVideoContext(`video_${this.objId}`, this);
|
||||
if(this.videoPlayId == this.objId){
|
||||
setTimeout(() => {
|
||||
this.videoCtx.play();
|
||||
this.playState = 2000;
|
||||
}, 200);
|
||||
}
|
||||
let throttling = true;
|
||||
uni.$on("videoProgress", res => {
|
||||
if(throttling){
|
||||
throttling = false;
|
||||
setTimeout(() => {
|
||||
throttling = true;
|
||||
if (res.progress == 1) {
|
||||
this.progressDrag = true;
|
||||
} else {
|
||||
this.progressDrag = false;
|
||||
}
|
||||
if (this.videoPlayId == this.objId && res.progressValue) {
|
||||
this.schedule = res.progressValue * 750;
|
||||
this.videoCtx.seek(this.duration * res.progressValue);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onScheduleChange(e) {
|
||||
this.duration = e.detail.duration;
|
||||
if (!this.progressDrag) {
|
||||
this.schedule = (parseFloat(e.detail.currentTime) / e.detail.duration) * 750;
|
||||
}
|
||||
},
|
||||
onPlay() {
|
||||
if (this.playState == 2000) {
|
||||
this.videoCtx.pause();
|
||||
this.playState = 1000;
|
||||
} else if (this.playState == 1000) {
|
||||
this.videoCtx.play();
|
||||
this.playState = 2000;
|
||||
} else if (this.playState == 3000) {
|
||||
this.videoCtx.play();
|
||||
this.playState = 2000;
|
||||
}
|
||||
},
|
||||
// 视频进入缓冲中
|
||||
onWaiting() {
|
||||
this.playState = 3000;
|
||||
},
|
||||
onError(e){
|
||||
console.log("视频播放出错", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/style/mixin.scss';
|
||||
.video_box {
|
||||
position: relative;
|
||||
width: 750rpx;
|
||||
}
|
||||
.bottom_mask {
|
||||
position: absolute;
|
||||
left: 0rpx;
|
||||
bottom: 0rpx;
|
||||
right: 0rpx;
|
||||
height: 300rpx;
|
||||
width: 750rpx;
|
||||
background-image: linear-gradient(to bottom, rgba(0,0,0,0) , rgba(0,0,0,0.7));
|
||||
}
|
||||
.video_file {
|
||||
width: 750rpx;
|
||||
}
|
||||
.play_schedule {
|
||||
position: absolute;
|
||||
bottom: 6rpx;
|
||||
left: 0;
|
||||
height: 16rpx;
|
||||
width: 750rpx;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.schedule_bg {
|
||||
position: absolute;
|
||||
top: 7rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2rpx;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.schedule {
|
||||
background-color: #fff;
|
||||
height: 2rpx;
|
||||
position: relative;
|
||||
}
|
||||
.progress_drag_dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
}
|
||||
.play_btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.icon_play {
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
}
|
||||
</style>
|
||||
330
components/stable-waterfall-goods/stable-waterfall-goods.vue
Normal file
330
components/stable-waterfall-goods/stable-waterfall-goods.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<view class="flow-box" :style="'height: ' + loadingTop + 'rpx'" ref="flow_box">
|
||||
<view v-for="(item, index) in newList" :key="item.objId" class="item good_item" v-show="positionList[item.objId].imageLoad" :class="positionList[item.objId].right ? 'right' : 'left'" :style="{top: positionList[item.objId].top + 'rpx'}"
|
||||
@click="onPageJump(item)">
|
||||
<view class="goods_img">
|
||||
<image :data-id="item.objId" @load="onImageLoad" :src="item.img" mode="widthFix"></image>
|
||||
</view>
|
||||
<view class="title">{{ item.name }}</view>
|
||||
<view class="sell_well">
|
||||
<text>热销</text>
|
||||
</view>
|
||||
<view class="info">
|
||||
<text class="money"><text>{{ item.priceDiscount }}</text></text>
|
||||
<text class="count">{{ item.numSales || 0 }}人购买</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const defaultData = {
|
||||
// 起始距离(rpx)
|
||||
startTop: 20,
|
||||
// 除了图片和标题以外的高度(rpx)
|
||||
contentHeight: 40 + 32 + 20 + 30 + 60,
|
||||
// 瀑布流容器宽度(rpx)
|
||||
viewWidth: 344,
|
||||
// 标题可显示宽度(rpx)
|
||||
titleWidth: 300,
|
||||
// 标题文字大小(rpx)
|
||||
titleSize: 28,
|
||||
// 容器之间的间隔Y轴(rpx)
|
||||
viewSpace: 20,
|
||||
// 列表ID参数名称
|
||||
idName: "objId",
|
||||
// 列表标题参数名称
|
||||
titleName: "name",
|
||||
};
|
||||
|
||||
// 文字换行计算行数
|
||||
function drawtext(text, maxWidth) {
|
||||
let textArr = text.split("");
|
||||
let len = textArr.length;
|
||||
// 上个节点
|
||||
let previousNode = 0;
|
||||
// 记录节点宽度
|
||||
let nodeWidth = 0;
|
||||
// 文本换行数组
|
||||
let rowText = [];
|
||||
// 如果是字母,侧保存长度
|
||||
let letterWidth = 0;
|
||||
// 汉字宽度
|
||||
let chineseWidth = defaultData.titleSize;
|
||||
// otherFont宽度
|
||||
let otherWidth = defaultData.titleSize / 2;
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (/[\u4e00-\u9fa5]|[\uFE30-\uFFA0]/g.test(textArr[i])) {
|
||||
if(letterWidth > 0){
|
||||
if(nodeWidth + chineseWidth + letterWidth * otherWidth > maxWidth){
|
||||
rowText.push({
|
||||
type: "text",
|
||||
content: text.substring(previousNode, i)
|
||||
});
|
||||
previousNode = i;
|
||||
nodeWidth = chineseWidth;
|
||||
letterWidth = 0;
|
||||
} else {
|
||||
nodeWidth += chineseWidth + letterWidth * otherWidth;
|
||||
letterWidth = 0;
|
||||
}
|
||||
} else {
|
||||
if(nodeWidth + chineseWidth > maxWidth){
|
||||
rowText.push({
|
||||
type: "text",
|
||||
content: text.substring(previousNode, i)
|
||||
});
|
||||
previousNode = i;
|
||||
nodeWidth = chineseWidth;
|
||||
}else{
|
||||
nodeWidth += chineseWidth;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(/\n/g.test(textArr[i])){
|
||||
rowText.push({
|
||||
type: "break",
|
||||
content: text.substring(previousNode, i)
|
||||
});
|
||||
previousNode = i + 1;
|
||||
nodeWidth = 0;
|
||||
letterWidth = 0;
|
||||
}else if(textArr[i] == "\\" && textArr[i + 1] == "n"){
|
||||
rowText.push({
|
||||
type: "break",
|
||||
content: text.substring(previousNode, i)
|
||||
});
|
||||
previousNode = i + 2;
|
||||
nodeWidth = 0;
|
||||
letterWidth = 0;
|
||||
}else if(/[a-zA-Z0-9]/g.test(textArr[i])){
|
||||
letterWidth += 1;
|
||||
if(nodeWidth + letterWidth * otherWidth > maxWidth){
|
||||
rowText.push({
|
||||
type: "text",
|
||||
content: text.substring(previousNode, i + 1 - letterWidth)
|
||||
});
|
||||
previousNode = i + 1 - letterWidth;
|
||||
nodeWidth = letterWidth * otherWidth;
|
||||
letterWidth = 0;
|
||||
}
|
||||
} else{
|
||||
if(nodeWidth + otherWidth > maxWidth){
|
||||
rowText.push({
|
||||
type: "text",
|
||||
content: text.substring(previousNode, i)
|
||||
});
|
||||
previousNode = i;
|
||||
nodeWidth = otherWidth;
|
||||
}else{
|
||||
nodeWidth += otherWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (previousNode < len) {
|
||||
rowText.push({
|
||||
type: "text",
|
||||
content: text.substring(previousNode, len)
|
||||
});
|
||||
}
|
||||
return rowText.length;
|
||||
}
|
||||
let allow = true;
|
||||
export default {
|
||||
props: {
|
||||
// 数据列表
|
||||
list: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newList: [],
|
||||
positionList: {},
|
||||
loadingTop: 0,
|
||||
leftHistoryTop: 0,
|
||||
rightHistoryTop: 0
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// 数据
|
||||
list: function(newVal, oldVal) {
|
||||
this.newList = newVal;
|
||||
this.getCalculationPosition();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onPageJump(item) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/home/shop/goodsDetail?objId=" + item.objId
|
||||
})
|
||||
},
|
||||
refreshData() {},
|
||||
// 节流
|
||||
throttle(callback,time = 400){
|
||||
if(!allow) return false;
|
||||
allow = false;
|
||||
setTimeout(function(){
|
||||
allow = true;
|
||||
callback();
|
||||
},time);
|
||||
},
|
||||
// 获取数据位置信息 top,right
|
||||
getCalculationPosition(){
|
||||
let leftHistoryTop = defaultData.startTop;
|
||||
let rightHistoryTop = defaultData.startTop;
|
||||
let positionList = this.positionList;
|
||||
this.newList.forEach((item,index) => {
|
||||
let currentHeight = defaultData.contentHeight;
|
||||
let positionItem = {};
|
||||
// 查看是否有位置数据
|
||||
if(positionList[item[defaultData.idName]]){
|
||||
positionItem = positionList[item[defaultData.idName]];
|
||||
}else {
|
||||
positionList[item[defaultData.idName]] = {};
|
||||
}
|
||||
// 查看是否有图片高度数据,没有默认等宽
|
||||
if(positionItem.imageHeight){
|
||||
currentHeight += positionItem.imageHeight;
|
||||
positionList[item[defaultData.idName]].imageLoad = true;
|
||||
}else{
|
||||
currentHeight += defaultData.viewWidth;
|
||||
}
|
||||
// 查看是否有标题高度数据,没有获取高度
|
||||
if(positionItem.titleHeight){
|
||||
currentHeight += positionItem.titleHeight;
|
||||
}else{
|
||||
let titleListLen = drawtext(item[defaultData.titleName], defaultData.titleWidth);
|
||||
titleListLen = titleListLen > 2 ? 2 : titleListLen;
|
||||
positionList[item[defaultData.idName]].titleHeight = titleListLen * (defaultData.titleSize + 6);
|
||||
currentHeight += titleListLen * (defaultData.titleSize + 6);
|
||||
}
|
||||
if(leftHistoryTop > rightHistoryTop){
|
||||
positionList[item[defaultData.idName]].top = rightHistoryTop;
|
||||
positionList[item[defaultData.idName]].right = true;
|
||||
positionList[item[defaultData.idName]].height = currentHeight;
|
||||
rightHistoryTop += currentHeight + defaultData.viewSpace;
|
||||
}else{
|
||||
positionList[item[defaultData.idName]].top = leftHistoryTop;
|
||||
positionList[item[defaultData.idName]].right = false;
|
||||
positionList[item[defaultData.idName]].height = currentHeight;
|
||||
leftHistoryTop += currentHeight + defaultData.viewSpace;
|
||||
}
|
||||
});
|
||||
if(leftHistoryTop > rightHistoryTop){
|
||||
this.loadingTop = leftHistoryTop;
|
||||
}else{
|
||||
this.loadingTop = rightHistoryTop;
|
||||
}
|
||||
this.positionList = positionList;
|
||||
this.$forceUpdate();
|
||||
},
|
||||
// 图片加载完成
|
||||
onImageLoad(e){
|
||||
let id = e.currentTarget.dataset.id;
|
||||
let scale = defaultData.viewWidth / e.detail.width;
|
||||
let height = scale * e.detail.height;
|
||||
if(this.positionList[id]){
|
||||
this.positionList[id].imageHeight = height;
|
||||
}else{
|
||||
this.positionList[id] = {
|
||||
imageHeight: height
|
||||
}
|
||||
}
|
||||
// 图片加载完刷新位置节流一下,提升性能
|
||||
this.throttle(() => {
|
||||
this.getCalculationPosition();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/style/mixin.scss';
|
||||
|
||||
.flow-box {
|
||||
position: relative;
|
||||
color: #1a1a1a;
|
||||
padding: 0 20rpx 0rpx 20rpx;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.flow-box .left {
|
||||
left: 20rpx;
|
||||
}
|
||||
|
||||
.flow-box .right {
|
||||
right: 20rpx;
|
||||
}
|
||||
|
||||
.flow-box .good_item {
|
||||
position: absolute;
|
||||
width: 345rpx;
|
||||
border: 2rpx solid #f9f9f9;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&.noMargin {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.goods_img {
|
||||
image {
|
||||
width: 100%;
|
||||
height: 345rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 20rpx 20rpx;
|
||||
color: #333333;
|
||||
font-size: 28rpx;
|
||||
@include bov(2);
|
||||
}
|
||||
.sell_well {
|
||||
display: flex;
|
||||
padding: 0rpx 20rpx 20rpx 20rpx;
|
||||
|
||||
text {
|
||||
height: 32rpx;
|
||||
border-radius: 4rpx;
|
||||
border: solid 2rpx $themeColor;
|
||||
line-height: 28rpx;
|
||||
padding: 0 15rpx;
|
||||
font-size: 24rpx;
|
||||
color: $themeColor;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 100%;
|
||||
padding: 0rpx 20rpx 30rpx 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.money {
|
||||
font-size: 26rpx;
|
||||
color: #ff3d3d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
text {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
128
components/virtualList/virtualList.vue
Normal file
128
components/virtualList/virtualList.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<view class="virtual-list" style="position: relative;">
|
||||
<scroll-view scroll-y="true"
|
||||
:style="{
|
||||
'height': scrollHeight + 'px',
|
||||
'position': 'relative',
|
||||
'zIndex': 1
|
||||
}"
|
||||
@scroll="handleScroll" :scroll-top="scrollTop" :show-scrollbar="false">
|
||||
|
||||
<view class="scroll-bar"
|
||||
:style="{
|
||||
'height': localHeight + 'px'
|
||||
}"></view>
|
||||
<view class="list"
|
||||
:style="{
|
||||
'transform': `translateY(${offset}px)`
|
||||
}">
|
||||
<view class="item-wrap"
|
||||
v-for="(item, index) in visibleData"
|
||||
:key="index">
|
||||
<slot :item="item" :active="active"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'VirtualList',
|
||||
props: {
|
||||
// 所有的items
|
||||
items: Array,
|
||||
// 可视区域的item数量
|
||||
remain: Number,
|
||||
// item大小
|
||||
size: Number,
|
||||
// 当前章节
|
||||
active: Number,
|
||||
// 可使区域高度
|
||||
scrollHeight: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 起始
|
||||
start: 0,
|
||||
// 结束
|
||||
end: this.remain,
|
||||
// list 偏移量
|
||||
offset: 0,
|
||||
scrollTop: 0,
|
||||
y: 0
|
||||
}
|
||||
},
|
||||
created() {
|
||||
//当前章节滚动至顶部
|
||||
this.scrollTop = this.size * this.active
|
||||
console.log('data===>',this.active)
|
||||
},
|
||||
computed: {
|
||||
// 预留项
|
||||
preCount() {
|
||||
return Math.min(this.start, this.remain);
|
||||
},
|
||||
nextCount() {
|
||||
return Math.min(this.items.length - this.end, this.remain);
|
||||
},
|
||||
// 可视区域的item
|
||||
visibleData() {
|
||||
const start = this.start - this.preCount;
|
||||
const end = this.end + this.nextCount;
|
||||
return this.items.slice(start, end);
|
||||
},
|
||||
localHeight() {
|
||||
return this.items.length * this.size
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change(e) {
|
||||
if (e.detail.source !== 'touch') {
|
||||
return
|
||||
}
|
||||
let y = e.detail.y;
|
||||
let scroll = y/(this.scrollHeight-40)*(this.localHeight-this.scrollHeight);
|
||||
scroll = scroll < 0 ? 0 : scroll;
|
||||
this.scrollTop = scroll;
|
||||
},
|
||||
handleScroll(ev) {
|
||||
const scrollTop = ev.detail.scrollTop;
|
||||
this.y = scrollTop/(this.localHeight-this.scrollHeight)*(this.scrollHeight-40)
|
||||
// 开始位置
|
||||
const start = Math.floor(scrollTop / this.size)
|
||||
this.start = start < 0 ? 0 : start;
|
||||
// 结束位置
|
||||
this.end = this.start + this.remain;
|
||||
// 计算偏移
|
||||
const offset = scrollTop - (scrollTop % this.size) - this.preCount * this.size
|
||||
this.offset = offset < 0 ? 0 : offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.list {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.action-bar-box{
|
||||
padding: 3px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background-color: transparent;
|
||||
border-radius: 10rpx;
|
||||
box-shadow: 0 0 5px #000;
|
||||
width: 20px;
|
||||
height: 40px;
|
||||
z-index:2;
|
||||
}
|
||||
</style>
|
||||
161
components/z-navigation/z-navigation.vue
Normal file
161
components/z-navigation/z-navigation.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="footer_box" :class="{ footer_bg: bg }">
|
||||
<view v-for="(item, index) of navigationList" :key="index" class="footer_item">
|
||||
<view class="footer_nav_item" @click="onPageJump(item.pagePath)">
|
||||
<image v-if="item.pagePath == path" class="footer_nav_item_image footer_nav_item_image_scale"
|
||||
:src="'/' + item.selectedIconPath" mode="aspectFit"></image>
|
||||
<image v-else class="footer_nav_item_image" :src="'/' + item.iconPath" mode="aspectFit"></image>
|
||||
<text class="footer_nav_item_text"
|
||||
:class="[item.pagePath == path ? 'footer_item_text_active' : '']">{{ item.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="bg" class="footer_station"></view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
bg: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
path: '',
|
||||
navigationList: [{
|
||||
pagePath: 'pages/peanut/home',
|
||||
iconPath: 'static/tab/icon1_n.png',
|
||||
selectedIconPath: 'static/tab/icon1_y.png',
|
||||
text: '首页'
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/peanut/shopping',
|
||||
iconPath: 'static/tab/icon2_n.png',
|
||||
selectedIconPath: 'static/tab/icon2_y.png',
|
||||
text: '购物车'
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/peanut/bookshelf',
|
||||
iconPath: 'static/tab/icon3_n.png',
|
||||
selectedIconPath: 'static/tab/icon3_y.png',
|
||||
text: '我的书架'
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/peanut/mine',
|
||||
iconPath: 'static/tab/icon4_n.png',
|
||||
selectedIconPath: 'static/tab/icon4_y.png',
|
||||
text: '我的'
|
||||
}
|
||||
],
|
||||
};
|
||||
},
|
||||
//第一次加载
|
||||
created() {
|
||||
//获取所有页面
|
||||
let currentPages = getCurrentPages();
|
||||
let page = currentPages[currentPages.length - 1];
|
||||
this.path = page.route;
|
||||
},
|
||||
//方法
|
||||
methods: {
|
||||
onPageJump(url) {
|
||||
if (this.path !== url) {
|
||||
uni.switchTab({
|
||||
url: '/' + url
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/style/mixin.scss';
|
||||
|
||||
.footer_station {
|
||||
height: 110rpx;
|
||||
box-sizing: content-box;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.footer_box {
|
||||
height: 110rpx;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
z-index: 502;
|
||||
box-sizing: content-box;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.footer_bg {
|
||||
background-color: #FFF;
|
||||
box-shadow: 0 0px 10px 1px #0000001a;
|
||||
}
|
||||
|
||||
.footer_item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.footer_nav_item {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.footer_nav_item:active {
|
||||
background-color: rgba($color: #fff, $alpha: 0.1);
|
||||
}
|
||||
|
||||
.footer_nav_item_text {
|
||||
font-size: 26rpx;
|
||||
color: #909090;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.footer_nav_item_text_active {
|
||||
color: #f9a633;
|
||||
}
|
||||
|
||||
.footer_nav_item_image {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
|
||||
}
|
||||
|
||||
.footer_nav_item_image_scale {
|
||||
animation: mescrollUpRotate 0.6s linear 1;
|
||||
}
|
||||
|
||||
@keyframes mescrollUpRotate {
|
||||
0% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.footer_item_text_active {
|
||||
color: #079307;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user