319 lines
10 KiB
Vue
319 lines
10 KiB
Vue
<template>
|
||
<view class="component-wrap" :style="{ height: wrapHeight + 'px' }">
|
||
<view class="component-menu-wrap">
|
||
<scroll-view
|
||
scroll-y
|
||
class="component-tab-view menu-scroll-view"
|
||
:scroll-top="scrollTop"
|
||
:scroll-into-view="itemId"
|
||
>
|
||
<view
|
||
class="component-tab-item"
|
||
v-for="(item, index) in data"
|
||
:key="index"
|
||
:class="[
|
||
current == index ? 'component-tab-item-active' : '',
|
||
]"
|
||
@tap.stop="swichMenu(index)"
|
||
>
|
||
<text class="component-line-1">{{ item.name }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
<scroll-view
|
||
:scroll-top="scrollRightTop"
|
||
scroll-y
|
||
scroll-with-animation
|
||
class="right-box"
|
||
@scroll="rightScroll"
|
||
>
|
||
<view class="page-view" :class="[cateType]">
|
||
<view
|
||
class="class-item"
|
||
v-for="(item, index) in data"
|
||
:id="'item' + index"
|
||
:key="index"
|
||
:style="{
|
||
height:
|
||
index == data.length - 1
|
||
? cateType == 'cate'
|
||
? 'calc(100vh - 70rpx)'
|
||
: '100vh'
|
||
: 'auto',
|
||
}"
|
||
>
|
||
<cate-tmpl
|
||
v-if="cateType === 'cate'"
|
||
:data="item"
|
||
@clickCate="clickCate"
|
||
></cate-tmpl>
|
||
<list-tmpl
|
||
v-if="cateType === 'list'"
|
||
:data="item"
|
||
@clickItem="clickItem"
|
||
></list-tmpl>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { mapState } from 'vuex';
|
||
import CateTmpl from "@/components/cate/template/cate";
|
||
import ListTmpl from "@/components/cate/template/list";
|
||
export default {
|
||
name: "component-cate",
|
||
data() {
|
||
return {
|
||
scrollTop: 0, //tab标题的滚动条位置
|
||
oldScrollTop: 0, // tab标题的滚动条旧位置
|
||
current: 0, // 预设当前项的值
|
||
menuHeight: 0, // 左边菜单的高度
|
||
menuItemHeight: 50, // 左边菜单item的高度
|
||
itemId: "", // 栏目右边scroll-view用于滚动的id
|
||
arr: [], // 储存距离顶部高度的数组
|
||
scrollRightTop: 0, // 右边栏目scroll-view的滚动条高度
|
||
timer: null, // 定时器
|
||
wrapHeight: 0,
|
||
};
|
||
},
|
||
props: {
|
||
cateType: {
|
||
type: String,
|
||
default: "cate",
|
||
},
|
||
offsetHeight: {
|
||
type: Number,
|
||
default: 0,
|
||
},
|
||
data: {
|
||
type: Array,
|
||
default: [],
|
||
},
|
||
},
|
||
components: {
|
||
CateTmpl,
|
||
ListTmpl,
|
||
},
|
||
computed: {
|
||
...mapState({
|
||
config: state => state.system.config,
|
||
bodyPaddingTop: (state) => state.system.bodyPaddingTop,
|
||
})
|
||
},
|
||
created() {},
|
||
mounted() {
|
||
this.getMenuItemTop();
|
||
let headerHeight = this.$utils.rpx2px(this.bodyPaddingTop);
|
||
let offsetHeight = this.$utils.rpx2px(this.offsetHeight);
|
||
const { windowHeight, statusBarHeight } = this.config;
|
||
// 窗口高度 - 头部高度 - 顶部内容高度 = 分类容器高度
|
||
this.wrapHeight = windowHeight - headerHeight - offsetHeight;
|
||
// #ifdef MP-WEIXIN
|
||
// 微信窗口高度不会计算tabbar高度,这里加回来
|
||
if (this.cateType == 'cate') {
|
||
this.wrapHeight += this.$utils.rpx2px(100);
|
||
}
|
||
// #endif
|
||
},
|
||
destroyed() {},
|
||
methods: {
|
||
getElRect(elClass, dataVal) {
|
||
new Promise((resolve, reject) => {
|
||
const query = uni.createSelectorQuery().in(this);
|
||
query
|
||
.select("." + elClass)
|
||
.fields(
|
||
{
|
||
size: true,
|
||
},
|
||
(res) => {
|
||
// 如果节点尚未生成,res值为null,循环调用执行
|
||
if (!res) {
|
||
this.getElRect(elClass);
|
||
return;
|
||
}
|
||
this[dataVal] = res.height;
|
||
resolve();
|
||
}
|
||
)
|
||
.exec();
|
||
});
|
||
},
|
||
/**
|
||
* 获取右边菜单每个item到顶部的距离
|
||
* 储存到 arr 数组里面用于后面滚动判断
|
||
*/
|
||
getMenuItemTop() {
|
||
new Promise((resolve) => {
|
||
let selectorQuery = uni.createSelectorQuery().in(this);
|
||
selectorQuery
|
||
.selectAll(".class-item")
|
||
.boundingClientRect((rects) => {
|
||
// 如果节点尚未生成,rects值为[](因为用selectAll,所以返回的是数组),循环调用执行
|
||
if (!rects.length) {
|
||
this.getMenuItemTop();
|
||
return;
|
||
}
|
||
rects.forEach((rect) => {
|
||
// 视情况而定,这里减去rects[0].top,是因为第一项顶部可能不是贴到导航栏(比如有个搜索框的情况)
|
||
this.arr.push(rect.top - rects[0].top);
|
||
// this.arr.push(rect.top)
|
||
resolve();
|
||
});
|
||
})
|
||
.exec();
|
||
});
|
||
},
|
||
/**
|
||
* 观测元素相交状态
|
||
* 检测右边scroll-view的id为itemxx的元素与right-box的相交状态
|
||
* 如果跟.right-box底部相交,就动态设置左边栏目的活动状态
|
||
*/
|
||
async observer() {
|
||
this.data.map((val, index) => {
|
||
let observer = uni.createIntersectionObserver(this);
|
||
observer
|
||
.relativeTo(".right-box", {
|
||
top: 0,
|
||
})
|
||
.observe("#item" + index, (res) => {
|
||
if (res.intersectionRatio > 0) {
|
||
let id = res.id.substring(4);
|
||
this.leftMenuStatus(id);
|
||
}
|
||
});
|
||
});
|
||
},
|
||
/**
|
||
* 设置左边菜单的滚动状态
|
||
* @index 传入的 ID
|
||
*/
|
||
async leftMenuStatus(index) {
|
||
this.current = index;
|
||
// 如果为0,意味着尚未初始化
|
||
if (this.menuHeight == 0 || this.menuItemHeight == 0) {
|
||
await this.getElRect("menu-scroll-view", "menuHeight");
|
||
await this.getElRect("component-tab-item", "menuItemHeight");
|
||
}
|
||
// 将菜单活动item垂直居中
|
||
this.scrollTop =
|
||
index * this.menuItemHeight +
|
||
this.menuItemHeight / 2 -
|
||
this.menuHeight / 2;
|
||
},
|
||
/**
|
||
* 点击左边的栏目切换
|
||
* @index 传入的 ID
|
||
*/
|
||
async swichMenu(index) {
|
||
if (this.arr.length == 0) {
|
||
await this.getMenuItemTop();
|
||
}
|
||
if (index == this.current) return;
|
||
this.scrollRightTop = this.oldScrollTop;
|
||
this.$nextTick(function () {
|
||
this.scrollRightTop = this.arr[index];
|
||
this.current = index;
|
||
this.leftMenuStatus(index);
|
||
});
|
||
},
|
||
/**
|
||
* 右边菜单滚动
|
||
* 如果不存在height2,意味着数据循环已经到了最后一个,设置左边菜单为最后一项即可
|
||
*/
|
||
async rightScroll(e) {
|
||
this.oldScrollTop = e.detail.scrollTop;
|
||
if (this.arr.length == 0) {
|
||
await this.getMenuItemTop();
|
||
}
|
||
if (!this.menuHeight) {
|
||
await this.getElRect("menu-scroll-view", "menuHeight");
|
||
}
|
||
// scrollHeight为右边菜单垂直中点位置
|
||
// let scrollHeight = e.detail.scrollTop + this.menuHeight / 2;
|
||
// scrollHeight为右边菜单头部位置
|
||
let scrollHeight = e.detail.scrollTop + 15;
|
||
for (let i = 0; i < this.arr.length; i++) {
|
||
let height1 = this.arr[i];
|
||
let height2 = this.arr[i + 1];
|
||
if (
|
||
!height2 ||
|
||
(scrollHeight >= height1 && scrollHeight < height2)
|
||
) {
|
||
this.leftMenuStatus(i);
|
||
return;
|
||
}
|
||
}
|
||
},
|
||
clickCate(id) {
|
||
this.$emit("clickCate", id);
|
||
},
|
||
clickItem(id) {
|
||
this.$emit("clickItem", id);
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.component-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
}
|
||
.component-search-box {
|
||
padding: 18rpx 30rpx;
|
||
}
|
||
.component-menu-wrap {
|
||
flex: 1;
|
||
display: flex;
|
||
overflow: hidden;
|
||
}
|
||
.component-tab-view {
|
||
width: 200rpx;
|
||
height: 100%;
|
||
background-color: #f6f6f6;
|
||
}
|
||
.component-tab-item {
|
||
height: 100rpx;
|
||
box-sizing: border-box;
|
||
padding-left: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 24rpx;
|
||
color: #8b8b8b;
|
||
font-weight: 400;
|
||
line-height: 1;
|
||
}
|
||
.component-tab-item-active {
|
||
position: relative;
|
||
color: #4b65ed;
|
||
background: #ffffff;
|
||
}
|
||
.component-tab-item-active::before {
|
||
content: "";
|
||
position: absolute;
|
||
border-left: 4px solid #4b65ed;
|
||
height: 24rpx;
|
||
left: 0;
|
||
top: 38rpx;
|
||
}
|
||
.component-tab-view {
|
||
height: 100%;
|
||
}
|
||
.right-box {
|
||
width: 550rpx;
|
||
}
|
||
.page-view.cate {
|
||
padding-right: 16rpx;
|
||
}
|
||
.page-view.list {
|
||
padding-right: 0;
|
||
}
|
||
.class-item {
|
||
background-color: #ffffff;
|
||
padding: 36rpx 32rpx 0 32rpx;
|
||
}
|
||
</style> |