|
|
<template>
|
|
|
<view class="y-tab__pane" :data-index="index" :class="[uniquePaneClass, paneClass]" :style="[paneStyle]"
|
|
|
@touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd">
|
|
|
<!-- 渲染过的则不再渲染,未渲染的根据激活状态进行渲染 -->
|
|
|
<view class="y-tab__pane--wrap" v-if="rendered ? true : active">
|
|
|
<slot></slot>
|
|
|
</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import { isNull, toClass, getUid } from '../js/uitls';
|
|
|
import { touchMixin } from '../js/touchMixin';
|
|
|
export default {
|
|
|
name: 'yTab',
|
|
|
mixins: [touchMixin],
|
|
|
options: {
|
|
|
// 微信小程序中 options 选项
|
|
|
styleIsolation: 'apply-shared', // [微信小程序] 启动样式隔离。当使用页面自定义组件,希望父组件影响子组件样式时可能需要配置。具体配置选项参见:微信小程序自定义组件的样式
|
|
|
virtualHost: true // [微信小程序、支付宝小程序(默认值为 true)] 将自定义节点设置成虚拟的,更加接近Vue组件的表现。我们不希望自定义组件的这个节点本身可以设置样式、响应 flex 布局等,而是希望自定义组件内部的第一层节点能够响应 flex 布局或者样式由自定义组件本身完全决定
|
|
|
// 微信(可以使用virtualHost配置)/QQ/百度/字节跳动这四家小程序,自定义组件在渲染时会比App/H5端多一级节点,导致flex无效,是否考虑在组件上增加class控制
|
|
|
},
|
|
|
props: {
|
|
|
title: String, // 标题
|
|
|
disabled: Boolean, // 是否禁用标签
|
|
|
dot: Boolean, // 是否在标题右上角显示小红点
|
|
|
badge: {
|
|
|
type: [Number, String],
|
|
|
default: ''
|
|
|
}, // 图标右上角徽标的内容
|
|
|
// 徽标数最大数字限制,超过这个数字将变成badgeMaxCount+,如果传空字符串则不设置
|
|
|
badgeMaxCount: {
|
|
|
type: [Number, String],
|
|
|
default: 99
|
|
|
},
|
|
|
name: [Number, String], // 标签名称,作为匹配的标识符
|
|
|
titleStyle: Object, // 自定义标题样式
|
|
|
titleClass: String, // 自定义标题类名
|
|
|
iconType: String, //图标图案,为uniapp扩展组件(uni-ui)下的uni-icons的type值,customPrefix用法等同
|
|
|
iconSize: {
|
|
|
type: Number,
|
|
|
default: 16
|
|
|
}, //图标大小
|
|
|
customPrefix: String, //自定义图标
|
|
|
imageSrc: String, //图片路径
|
|
|
imageMode: {
|
|
|
type: String,
|
|
|
default: 'scaleToFill',
|
|
|
validator(value) {
|
|
|
return [
|
|
|
'scaleToFill',
|
|
|
'aspectFit',
|
|
|
'aspectFill',
|
|
|
'widthFix',
|
|
|
'heightFix',
|
|
|
'top',
|
|
|
'bottom',
|
|
|
'center',
|
|
|
'left',
|
|
|
'right',
|
|
|
'top left',
|
|
|
'top right',
|
|
|
'bottom left',
|
|
|
'bottom right'
|
|
|
].includes(value);
|
|
|
}
|
|
|
}, //图片裁剪、缩放的模式,为uniapp内置组件->媒体组件—>image下的mode值
|
|
|
position: {
|
|
|
type: String,
|
|
|
default: 'right',
|
|
|
validator(value) {
|
|
|
return ['top', 'bottom', 'left', 'right'].includes(value);
|
|
|
}
|
|
|
} //如果存在图片或图标,标题围绕它们的位置
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
__isUnmounted: false,
|
|
|
index: -1, //内容卡片对应的下标
|
|
|
parent: null, //父元素实例
|
|
|
active: false, //是否为激活状态
|
|
|
rendered: false, //是否渲染过
|
|
|
swipeable: false, //是否开启手势滑动切换
|
|
|
paneStyle: null, //内容样式
|
|
|
scrollspy: false, //是否为滚动导航模式
|
|
|
paneObserver: null, //pane交叉观察器
|
|
|
isDisjoint: false //当前pane是否与参照节点布局区域相离
|
|
|
};
|
|
|
},
|
|
|
computed: {
|
|
|
computedName() {
|
|
|
return !isNull(this.name) ? this.name : this.index;
|
|
|
},
|
|
|
// test
|
|
|
innerStyle() {
|
|
|
return {
|
|
|
position: 'absolute',
|
|
|
width: "100%",
|
|
|
height: "100%",
|
|
|
transform: `translate(${100 * this.index}%, 0px) translateZ(0px)`
|
|
|
}
|
|
|
},
|
|
|
// 保证唯一的样式
|
|
|
uniquePaneClass() {
|
|
|
return 'y-tab__pane' + getUid()
|
|
|
},
|
|
|
// 内容class
|
|
|
paneClass() {
|
|
|
return toClass({ 'is-active': this.active, 'is-scrollspy': this.scrollspy });
|
|
|
},
|
|
|
},
|
|
|
watch: {
|
|
|
$props: {
|
|
|
deep: true,
|
|
|
// immediate: true,
|
|
|
handler(newValue, oldValue) {
|
|
|
// 触发导航变化事件
|
|
|
if (this.parent) {
|
|
|
this.parent.updateTab({
|
|
|
newValue: { ...newValue, badge: this.formatBadge() },
|
|
|
oldValue: oldValue && { ...oldValue },
|
|
|
index: this.index
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
created() {
|
|
|
this.parent = this.getParent();
|
|
|
if (this.parent) {
|
|
|
this.parent.children.push(this);
|
|
|
this.parent.putTab({ newValue: { ...this.$props, badge: this.formatBadge() } });
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
|
// this.$nextTick(() => {
|
|
|
// this.observePane();
|
|
|
// });
|
|
|
},
|
|
|
// #ifndef VUE3
|
|
|
destroyed() {
|
|
|
if (this.__isUnmounted) return;
|
|
|
this.unInit();
|
|
|
},
|
|
|
// #endif
|
|
|
// #ifdef VUE3
|
|
|
unmounted() {
|
|
|
this.__isUnmounted = true;
|
|
|
this.unInit();
|
|
|
},
|
|
|
// #endif
|
|
|
methods: {
|
|
|
// 徽标格式化
|
|
|
formatBadge() {
|
|
|
if (!isNull(this.badge) && !isNull(this.badgeMaxCount) && this.badge > this.badgeMaxCount) {
|
|
|
return this.badgeMaxCount + '+';
|
|
|
} else {
|
|
|
return this.badge
|
|
|
}
|
|
|
},
|
|
|
// 获取查询节点信息的对象
|
|
|
getSelectorQuery() {
|
|
|
let query = null;
|
|
|
// #ifdef MP-ALIPAY
|
|
|
query = uni.createSelectorQuery();
|
|
|
// #endif
|
|
|
// #ifndef MP-ALIPAY
|
|
|
query = uni.createSelectorQuery().in(this);
|
|
|
// #endif
|
|
|
return query;
|
|
|
},
|
|
|
// 获取元素位置信息
|
|
|
getRect() {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
this.getSelectorQuery()
|
|
|
.select(`.${this.uniquePaneClass}`)
|
|
|
.boundingClientRect()
|
|
|
.exec(rect => {
|
|
|
console.log(rect);
|
|
|
resolve(rect[0] || {});
|
|
|
});
|
|
|
});
|
|
|
},
|
|
|
// 卸载组件的处理
|
|
|
unInit() {
|
|
|
this.disconnectObserver('paneObserver'); //销毁观察器
|
|
|
if (this.parent) {
|
|
|
const index = this.parent.children.findIndex(item => item === this);
|
|
|
this.parent.children.splice(index, 1);
|
|
|
this.parent.tabs.splice(index, 1);
|
|
|
this.parent.tabRects.splice(index, 1);
|
|
|
}
|
|
|
},
|
|
|
//获取父元素实例
|
|
|
getParent(name = 'yTabs') {
|
|
|
let parent = this.$parent;
|
|
|
let parentName = parent.$options.name;
|
|
|
while (parentName !== name) {
|
|
|
parent = parent.$parent;
|
|
|
if (!parent) return false;
|
|
|
parentName = parent.$options.name;
|
|
|
}
|
|
|
return parent;
|
|
|
},
|
|
|
// 断掉观察,释放资源
|
|
|
disconnectObserver(observerName) {
|
|
|
const observer = this[observerName];
|
|
|
observer && observer.disconnect();
|
|
|
},
|
|
|
// 观察 - 标签内容滚动时定位标签项
|
|
|
async observePane(top) {
|
|
|
if (!this.scrollspy) return //非滚动导航时不创建观察器
|
|
|
this.disconnectObserver('paneObserver');
|
|
|
const paneObserver = uni.createIntersectionObserver(this, { thresholds: [0, 1] });
|
|
|
|
|
|
|
|
|
// 注意:如果是区域滚动,pane区域随着页面滚动了一部分,那么y-tabs__content的top就会变化,导致交互区域位置不准确
|
|
|
// 最好不要有这种交互布局,不像Android有嵌套滑动机制,页面滑动到底后无法将事件分发给scroll-view
|
|
|
|
|
|
paneObserver.relativeToViewport({ top: -top }); // 到屏幕顶部的高度时触发
|
|
|
// 不能观察根节点 unk-vendors.js:14596 [system] Node .y-tab__pane9 is not found. Intersection observer will not trigger.
|
|
|
paneObserver.observe(`.${this.uniquePaneClass} .y-tab__pane--wrap`, res => {
|
|
|
// console.log('res:', this.title, res);
|
|
|
// 如果目标节点布局区域小于参照节点布局区域top,则说明目标节点在参照节点布局区域之上,intersectionRatio小于等于0则说明两者不相交了
|
|
|
this.isDisjoint = res.intersectionRatio <= 0 && res.boundingClientRect.top < res.relativeRect.top;
|
|
|
|
|
|
// 标签栏点击时触发的滚动不允许设置激活下标
|
|
|
if (!this.parent.lockedScrollspy) this.parent.setActivedIndexToScroll();
|
|
|
});
|
|
|
this.paneObserver = paneObserver;
|
|
|
},
|
|
|
}
|
|
|
};
|
|
|
</script>
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
@import url('../css/index.less');
|
|
|
</style> |