wx-mini-weview
Version:
weixin UI
993 lines (843 loc) • 31.2 kB
JavaScript
const { WvInheritable } = require('../../behaviors/WvInheritable');
const { WvComponentEvent } = require('../../behaviors/WvComponentEvent');
Component({
options: {
styleIsolation: 'apply-shared',
multipleSlots: true
},
relations: {
'../vtabbar/vtabbar': {
type: 'child',
linked(target) {
this.linkChildNode('vtabbar', target);
this._initTabbar();
},
linkChanged() {
},
unlinked(target) {
this.unlinkChildNode('vtabbar', target);
this._resetTabbar();
}
},
'../tabpanel-content/tabpanel-content': {
type: 'child',
linked(target) {
this.linkChildNode('tabpanelContent', target);
this._initContent();
// 当内容项被链接时,执行可见性检测
if (this.data.contentsMode === 'continuous') {
this._detectVisibleContents(this.data.containerScrollTop);
// 监听appear事件,在continuous模式下同步tabbar激活状态
target.on('appear', (detail) => {
// 当内容出现在视口中时,同步激活对应的标签
if (this.data.contentsMode === 'continuous') {
const tabbar = this._getTabbar();
if (tabbar && detail.index !== undefined) {
// 静默更新tabbar激活状态,不触发额外的内容切换
tabbar.setActiveTabSilent(detail.index);
// 更新当前索引(但不触发内容切换动作,因为内容已经可见)
if (this.data.currentIndex !== detail.index) {
this.setData({ currentIndex: detail.index });
}
}
}
}, 'vtabpanel');
}
},
linkChanged() {
},
unlinked(target) {
// 取消监听
target.off && target.off('appear', 'vtabpanel');
this.unlinkChildNode('tabpanelContent', target);
this._initContent();
}
}
},
behaviors: [
WvInheritable({
properties: {},
childrenProperties: {}
}),
WvComponentEvent
],
properties: {
active: {
type: null,
value: 0,
observer(newVal, oldVal) {
if (newVal !== oldVal) {
this.setActive(newVal);
}
}
},
background: {
type: String,
value: ''
},
gap: {
type: String,
value: '0'
},
// 整个 vTabPanel 的高度
height: {
type: String,
value: '300rpx'
},
// vTabBar 的位置,可以是 'left' 或 'right'
tabbarPosition: {
type: String,
value: 'left'
},
loop: {
type: Boolean,
value: false
},
autoplay: {
type: Boolean,
value: false
},
duration: {
type: Number,
value: 200
},
interval: {
type: Number,
value: 3000
},
// ================ 内容区域相关属性(content前缀)================
contentBackground: {
type: String,
value: 'rgba(var(--wv-theme-light-rgb), 1)'
},
contentBorderRadius: {
type: String,
value: '0'
},
contentIndicatorDots: {
type: Boolean,
value: true
},
contentIndicatorColor: {
type: String,
value: 'rgba(0, 0, 0, .3)'
},
contentIndicatorActiveColor: {
type: String,
value: '#000'
},
contentIndicatorActiveStyle: {
type: String,
value: 'longer', // scale || longer
},
// contentWidth为空的时候,表示随flex布局自然而然成为wv-vtabpanel - vtabbar部分的宽度也就是flex: 1的结果,如果指定了,则拥有自己的宽度
contentWidth: {
type: String,
value: ''
},
// 内容展示模式:continuous(连续)或 independent(独立)
contentsMode: {
type: String,
value: 'independent' // 'continuous' | 'independent'
},
customClass: {
type: String,
value: ''
},
// 是否可滑动切换内容
contentSwipeable: {
type: Boolean,
value: true
},
},
data: {
currentIndex: 0,
contentItemCount: 0,
isAnimating: false,
contentHeight: 0,
autoplayTimer: null,
// 添加防抖计时器
_debounceInitContentTimer: null,
_debounceUpdateContentTimer: null,
_debounceSetActiveTimer: null,
// 添加滚动控制变量
containerScrollTop: 0,
// 添加滚动节流控制变量
_scrollThrottleTimer: null,
_lastScrollTime: 0,
// 添加标志,避免滚动时重复触发
_isScrollingProgrammatically: false,
_detectVisibleContentsDebounceTimer: null,
_lastVisibilityScrollTop: null,
_lastVisibilityDetectionTime: null,
_lastFullyVisibleTimestamps: null,
// 添加触摸事件相关变量
touchStartX: 0,
touchStartY: 0,
touchDeltaY: 0,
isTouching: false
},
lifetimes: {
attached() {
},
ready() {
this._getContentHeight()
this._startAutoplay();
},
detached() {
// 清除定时器
if (this.data.autoplayTimer) {
clearInterval(this.data.autoplayTimer);
}
}
},
methods: {
/**
* 初始化内容组件 - 主要初始化入口
* @private
*/
_initContent() {
if (this.data._debounceInitContentTimer) {
clearTimeout(this.data._debounceInitContentTimer);
}
this.data._debounceInitContentTimer = setTimeout(() => {
const contentNodes = this.getChildrenNodes('tabpanelContent');
if (!contentNodes || !contentNodes.length) return;
// Synchronize currentIndex with active property if needed
const activeValue = this.data.active;
if (typeof activeValue === 'number' && activeValue !== this.data.currentIndex) {
if (activeValue >= 0 && activeValue < contentNodes.length) {
this.setData({ currentIndex: activeValue });
}
} else if (typeof activeValue === 'string') {
// Handle string active values (named tabs)
const foundIndex = contentNodes.findIndex(content =>
content.data.name === activeValue ||
content.data.wvInheritableApply?.name === activeValue);
if (foundIndex !== -1 && foundIndex !== this.data.currentIndex) {
this.setData({ currentIndex: foundIndex });
}
}
this.setData({
contentItemCount: contentNodes.length
}, () => {
this._updateContent();
});
}, 100);
},
/**
* 初始化标签栏组件
* @private
*/
_initTabbar() {
const tabbar = this._getTabbar();
if (!tabbar) return;
// 同步 active 状态到标签栏
tabbar.setData({
active: this.data.active
});
tabbar.on('change', (detail) => {
if (detail.targetIndex !== undefined && detail.targetIndex !== this.data.currentIndex) {
this.setActive(detail.targetIndex, 'vtabbar');
}
}, 'vtabpanel');
},
/**
* 清理标签栏事件监听
* @private
*/
_resetTabbar() {
const tabbar = this._getTabbar();
if (tabbar) {
// 取消监听
tabbar.off('change', 'vtabpanel');
}
},
/**
* 获取标签栏组件实例
* @private
* @return {Object|null} 标签栏组件实例
*/
_getTabbar() {
const tabbarNodes = this.getChildrenNodes('vtabbar');
return tabbarNodes && tabbarNodes.length > 0 ? tabbarNodes[0] : null;
},
/**
* 获取内容区域的高度
* @private
*/
_getContentHeight() {
const query = this.createSelectorQuery();
query.select('.wv-vtabpanel-content-wrapper').boundingClientRect();
query.exec(res => {
if (res && res[0]) {
this.setData({
contentHeight: res[0].height
});
}
});
},
/**
* 更新内容显示
* @private
*/
_updateContent() {
if (this.data._debounceUpdateContentTimer) {
clearTimeout(this.data._debounceUpdateContentTimer);
}
this.data._debounceUpdateContentTimer = setTimeout(() => {
const { currentIndex, contentsMode } = this.data;
const contentNodes = this.getChildrenNodes('tabpanelContent');
if (!contentNodes || !contentNodes.length) return;
contentNodes.forEach((content, index) => {
const isActive = index === currentIndex;
// 设置内容的激活状态
content.setActive(isActive, index);
if (contentsMode === 'independent') {
const style = isActive ? '' : 'display: none;';
content.updateStyle(style);
} else if (contentsMode === 'continuous') {
content.updateStyle('position: relative; display: block; transform: none; height: auto; ');
}
});
if (contentsMode === 'continuous') {
this._scrollToActiveContent();
}
}, 10);
},
/**
* 处理容器滚动事件,更新滚动位置
* @param {Object} e 滚动事件对象
*/
onContainerScroll(e) {
// 使用节流控制处理频率,避免频繁更新影响性能
const now = Date.now();
const throttleDelay = 100; // 节流延迟时间,单位毫秒
if (this.data._scrollThrottleTimer) {
clearTimeout(this.data._scrollThrottleTimer);
}
// 如果距离上次更新超过节流时间,立即更新
if (now - this.data._lastScrollTime > throttleDelay) {
this.data._lastScrollTime = now;
this._updateScrollPosition(e.detail.scrollTop);
this._detectVisibleContents(e.detail.scrollTop);
} else {
// 否则延迟更新,确保最后一次滚动也被记录
this.data._scrollThrottleTimer = setTimeout(() => {
this._updateScrollPosition(e.detail.scrollTop);
this._detectVisibleContents(e.detail.scrollTop);
this.data._lastScrollTime = Date.now();
}, throttleDelay);
}
},
/**
* 更新滚动位置
* @param {Number} scrollTop 滚动位置
* @private
*/
_updateScrollPosition(scrollTop, toView = false) {
if(toView){
// 使用this.setData({})会引起渲染
this.setData({ containerScrollTop: scrollTop });
}else{
// 直接更新data属性,避免触发不必要的渲染
this.data.containerScrollTop = scrollTop;
}
},
/**
* 检测当前可见的内容项
* @param {Number} scrollTop 当前滚动位置
* @private
*/
_detectVisibleContents(scrollTop) {
const now = Date.now();
const throttleDelay = 100; // 使用200毫秒的节流延迟,略高于滚动事件的100毫秒
if (this.data._detectVisibleContentsDebounceTimer) {
clearTimeout(this.data._detectVisibleContentsDebounceTimer);
}
// 记录最后调用的参数,以便延迟执行时使用最新值
this.data._lastVisibilityScrollTop = scrollTop;
// 如果距离上次更新超过节流时间,立即更新
if (!this.data._lastVisibilityDetectionTime || now - this.data._lastVisibilityDetectionTime > throttleDelay) {
this.data._lastVisibilityDetectionTime = now;
this._performVisibilityDetection(scrollTop);
} else {
// 否则延迟更新,确保最后一次滚动也被处理
this.data._detectVisibleContentsDebounceTimer = setTimeout(() => {
this.data._lastVisibilityDetectionTime = Date.now();
this._performVisibilityDetection(this.data._lastVisibilityScrollTop);
}, throttleDelay);
}
},
/**
* 执行可见性检测的实际逻辑
* @param {Number} scrollTop 当前滚动位置
* @private
*/
_performVisibilityDetection(scrollTop) {
if (this.data.contentsMode !== 'continuous') return;
const contentNodes = this.getChildrenNodes('tabpanelContent');
if (!contentNodes || !contentNodes.length) return;
// 确定滚动方向
const isScrollingUp = this.data._lastVisibilityScrollTop !== undefined &&
scrollTop < this.data._lastVisibilityScrollTop;
this.data._lastVisibilityScrollTop = scrollTop;
const query = this.createSelectorQuery();
query.select('.wv-vtabpanel-container.continuous-mode').boundingClientRect();
query.select('.wv-vtabpanel-container.continuous-mode').scrollOffset();
query.exec(res => {
if (!res || !res[0] || !res[1]) return;
const containerRect = res[0];
const containerVisibleTop = containerRect.top;
const containerVisibleBottom = containerRect.bottom;
const containerHeight = containerRect.bottom - containerRect.top;
// 获取正确的滚动高度和当前滚动位置
const scrollHeight = res[1].scrollHeight;
const currentScrollTop = res[1].scrollTop;
// 增加对底部检测的阈值,使其更容易触发
const bottomThreshold = 30; // 增加到30px
const isNearBottom = scrollHeight - currentScrollTop - containerHeight < bottomThreshold;
// 如果接近底部,直接激活最后一个内容并提前返回
if (isNearBottom && contentNodes.length > 0) {
const lastContent = contentNodes[contentNodes.length - 1];
this._updateAllContentsVisibility(lastContent);
return;
}
// 存储每个内容的可见率信息
const visibilityInfo = [];
// 创建所有内容节点的查询
const contentQueries = contentNodes.map(content => {
const contentQuery = content.createSelectorQuery();
contentQuery.select('.wv-tabpanel-content').boundingClientRect();
return { content, query: contentQuery };
});
// 初始化最大可见率追踪
let maxVisibleRatio = 0;
let mostVisibleContent = null;
// 记录完全可见的内容列表
const fullyVisibleContents = [];
// 如果当前没有最后完全可见内容的记录,初始化它
if (!this.data._lastFullyVisibleTimestamps) {
this.data._lastFullyVisibleTimestamps = new Map();
}
// 并行执行所有查询,收集信息
let pendingQueries = contentQueries.length;
contentQueries.forEach(({ content, query }) => {
query.exec(contentRes => {
if (!contentRes || !contentRes[0]) {
pendingQueries--;
return;
}
const contentRect = contentRes[0];
const contentHeight = contentRect.height;
// 检测元素是否在视口内
const isVisible = (
contentRect.bottom > containerVisibleTop &&
contentRect.top < containerVisibleBottom
);
let visibleRatio = 0;
if (isVisible) {
// 计算元素可见部分
const visibleHeight = Math.min(contentRect.bottom, containerVisibleBottom) -
Math.max(contentRect.top, containerVisibleTop);
visibleRatio = visibleHeight / contentHeight;
// 特殊处理超高元素
if (contentHeight > containerHeight * 1.5) {
// 对于非常高的元素,如果可见高度超过容器的70%,视为"足够可见"
if (visibleHeight > containerHeight * 0.7) {
visibleRatio = 1.0;
}
}
}
// 保存可见率信息
visibilityInfo.push({
content,
visibleRatio,
index: content.data.index
});
// 更新最大可见率追踪
if (visibleRatio > maxVisibleRatio) {
maxVisibleRatio = visibleRatio;
mostVisibleContent = content;
}
// 记录完全可见(或接近完全可见)的内容
if (visibleRatio >= 0.95) {
fullyVisibleContents.push(content);
// 更新最后完全可见时间戳
const now = Date.now();
this.data._lastFullyVisibleTimestamps.set(content, now);
}
pendingQueries--;
// 所有查询都已完成,现在处理结果
if (pendingQueries === 0) {
this._processVisibilityResults(visibilityInfo, fullyVisibleContents, mostVisibleContent, maxVisibleRatio);
}
});
});
});
},
/**
* 处理可见性检测结果,确定哪个content应该被标记为可见
* @private
*/
_processVisibilityResults(visibilityInfo, fullyVisibleContents, mostVisibleContent, maxVisibleRatio) {
// 1. 如果有完全可见的内容,选择最近成为完全可见的一个
if (fullyVisibleContents.length > 0) {
let latestFullyVisible = fullyVisibleContents[0];
let latestTimestamp = this.data._lastFullyVisibleTimestamps.get(latestFullyVisible) || 0;
fullyVisibleContents.forEach(content => {
const timestamp = this.data._lastFullyVisibleTimestamps.get(content) || 0;
if (timestamp > latestTimestamp) {
latestTimestamp = timestamp;
latestFullyVisible = content;
}
});
// 将最近完全可见的内容标记为可见
this._updateAllContentsVisibility(latestFullyVisible);
return;
}
// 2. 如果没有完全可见的内容,但有部分可见的内容
if (mostVisibleContent && maxVisibleRatio >= 0.3) {
this._updateAllContentsVisibility(mostVisibleContent);
return;
}
// 3. 边缘情况:如果所有内容的可见比例都未变化或都很低
// 查看是否接近容器边缘,决定是第一个还是最后一个内容
if (visibilityInfo.length > 0) {
const contentNodes = this.getChildrenNodes('tabpanelContent');
// 如果接近顶部,选择第一个内容
if (this.data.containerScrollTop < 10) {
const firstContent = contentNodes[0];
this._updateAllContentsVisibility(firstContent);
}
// 底部检测移到了函数开始处的query.exec回调中,因为这里需要更准确的滚动高度信息
}
},
/**
* 更新所有内容的可见性状态,只将指定的内容标记为可见
* @param {Object} visibleContent 应标记为可见的内容组件
* @private
*/
_updateAllContentsVisibility(visibleContent) {
const contentNodes = this.getChildrenNodes('tabpanelContent');
if (!contentNodes || !contentNodes.length) return;
contentNodes.forEach(content => {
const shouldBeVisible = content === visibleContent;
// 避免不必要的状态更新
if (content._appeared !== shouldBeVisible) {
content.updateVisibility(shouldBeVisible);
}
});
},
/**
* 仅同步标签栏状态,不触发内容滚动
* @private
*/
_syncTabbarOnly() {
const tabbar = this._getTabbar();
if (tabbar) {
tabbar.setActiveTabSilent(this.data.currentIndex);
}
},
/**
* 滚动到当前激活的内容
* @private
*/
_scrollToActiveContent() {
if (this.data.contentsMode !== 'continuous') return;
const contentNodes = this.getChildrenNodes('tabpanelContent');
if (!contentNodes?.length) return;
// First try to find by active flag, then by currentIndex
const activeContent = contentNodes.find(node => node.data.active) ||
(this.data.currentIndex < contentNodes.length ?
contentNodes[this.data.currentIndex] : null);
if (!activeContent) return;
const query = this.createSelectorQuery();
// 同时获取容器布局信息和滚动状态
query.select('.wv-vtabpanel-container').fields({
rect: true,
scrollOffset: true,
size: true
});
query.exec(res => {
const containerInfo = res?.[0];
if (!containerInfo) return;
// 获取容器关键参数
const {
top: containerTop, // 容器顶部视口位置
height: containerHeight, // 容器可视高度
scrollHeight, // 容器总高度
scrollTop: currentScroll // 当前滚动位置
} = containerInfo;
// 查询目标元素布局
const contentQuery = activeContent.createSelectorQuery();
contentQuery.select('.wv-tabpanel-content.active').boundingClientRect();
contentQuery.exec(contentRes => {
const targetRect = contentRes?.[0];
if (!targetRect) return;
// 计算元素相对容器的理论定位
const targetRelativeTop = targetRect.top - containerTop;
// 计算目标滚动位置(核心算法)
let targetScroll = currentScroll + targetRelativeTop;
// 边界处理:确保不超过可滚动范围
const maxScroll = scrollHeight - containerHeight;
targetScroll = Math.max(0, Math.min(targetScroll, maxScroll));
// 智能修正:当目标元素高度超过容器时
if (targetRect.height > containerHeight) {
// 如果当前在元素上半部分,对齐顶部;否则对齐底部
const visiblePart = currentScroll - (targetScroll - targetRect.height);
if (visiblePart > containerHeight / 2) {
targetScroll = Math.min(targetScroll + containerHeight, maxScroll);
}
}
// 应用滚动修正
this.data._isScrollingProgrammatically = true;
this._updateScrollPosition(targetScroll, true);
});
});
},
/**
* 切换到指定索引的内容
* @param {Number|String} index 目标索引或名称
* @param {String} source 事件来源
*/
setActive(index, source = 'code') {
// 添加防抖
if (this.data._debounceSetActiveTimer) {
clearTimeout(this.data._debounceSetActiveTimer);
}
this.data._debounceSetActiveTimer = setTimeout(() => {
const contentNodes = this.getChildrenNodes('tabpanelContent');
if (!contentNodes || !contentNodes.length) return;
let targetIndex = index;
// 如果提供的是名称,查找对应的索引
if (typeof index === 'string') {
const foundIndex = contentNodes.findIndex(content => content.data.name === index);
if (foundIndex !== -1) {
targetIndex = foundIndex;
} else {
return;
}
}
// 检查索引有效性
if (targetIndex < 0 || targetIndex >= contentNodes.length) {
if (!this.data.loop) return;
// 循环模式下处理边界情况
targetIndex = targetIndex < 0 ? contentNodes.length - 1 : 0;
}
// 触发切换前事件
const beforeChangeEvent = this.triggerEvent('beforeChange', {
current: this.data.currentIndex,
target: targetIndex
});
if (beforeChangeEvent && beforeChangeEvent.defaultPrevented) {
return;
}
// 更新状态
this.setData({
isAnimating: true,
currentIndex: targetIndex
}, () => {
// 更新内容显示
this._updateContent();
if (source !== 'vtabbar') {
this._syncTabbar();
}
// 触发切换后事件
this.triggerEvent('change', {
current: targetIndex,
source: source
});
setTimeout(() => {
this.setData({ isAnimating: false });
}, this.data.duration);
});
}, 10);
},
/**
* 同步标签栏状态
* @private
*/
_syncTabbar() {
const tabbar = this._getTabbar();
if (tabbar) {
tabbar.setActiveTab(this.data.currentIndex);
}
},
/**
* 切换到下一个内容
* @public
* @param {String} source 事件来源
*/
next(source = 'code') {
const { currentIndex, contentItemCount } = this.data;
let nextIndex = currentIndex + 1;
if (nextIndex >= contentItemCount && !this.data.loop) {
nextIndex = currentIndex
}
this.setActive(nextIndex, source);
},
/**
* 切换到上一个内容
* @public
* @param {String} source 事件来源
*/
prev(source = 'code') {
const { currentIndex } = this.data;
let prevIndex = currentIndex - 1;
if (prevIndex < 0 && !this.data.loop) {
prevIndex = currentIndex
}
this.setActive(prevIndex, source);
},
/**
* 启动自动播放
* @private
*/
_startAutoplay() {
if (this.data.autoplay && this.data.contentsMode === 'independent') {
if (this.data.autoplayTimer) {
clearInterval(this.data.autoplayTimer);
}
const timer = setInterval(() => {
this.next();
}, this.data.interval);
this.setData({ autoplayTimer: timer });
}
},
/**
* 停止自动播放
* @private
*/
_stopAutoplay() {
if (this.data.autoplayTimer) {
clearInterval(this.data.autoplayTimer);
this.setData({ autoplayTimer: null });
}
},
/**
* 触摸开始事件处理
* @param {Object} e 事件对象
*/
onTouchStart(e) {
if (!this.data.contentSwipeable || this.data.contentsMode === 'continuous') return;
const touch = e.touches[0];
this.setData({
touchStartX: touch.clientX,
touchStartY: touch.clientY,
touchDeltaY: 0,
isTouching: true
});
// 暂停自动播放
if (this.data.autoplay) {
this._stopAutoplay();
}
},
/**
* 触摸移动事件处理
* @param {Object} e 事件对象
*/
onTouchMove(e) {
if (!this.data.contentSwipeable || !this.data.isTouching || this.data.contentsMode === 'continuous') return;
const touch = e.touches[0];
const deltaX = touch.clientX - this.data.touchStartX;
const deltaY = touch.clientY - this.data.touchStartY;
// 如果是横向滑动,不进行纵向滑动处理
if (Math.abs(deltaX) > Math.abs(deltaY)) {
return;
}
// 边界检查:如果不是循环模式,在首尾项限制滑动
// if (!this.data.loop) {
// if ((this.data.currentIndex === 0 && deltaY > 0) ||
// (this.data.currentIndex === this.data.contentItemCount - 1 && deltaY < 0)) {
// return;
// }
// }
this.setData({
touchDeltaY: deltaY
});
// 计算滑动百分比
let maxPercentage = 100
const absPercentage = Math.min(Math.abs(deltaY) / this.data.contentHeight * 100, maxPercentage);
// 应用拖动效果到内容
this._applyDragEffect(deltaY);
// 计算指示器移动方向
const direction = deltaY < 0 ? 'down' : 'up';
// 调用vtabbar的shiftIndicator方法
const tabbar = this._getTabbar();
if (tabbar && typeof tabbar.shiftIndicator === 'function') {
tabbar.shiftIndicator(direction, absPercentage);
}
},
/**
* 触摸结束事件处理
* @param {Object} e 事件对象
*/
onTouchEnd(e) {
if (!this.data.contentSwipeable || !this.data.isTouching || this.data.contentsMode === 'continuous') return;
const { touchDeltaY, contentHeight } = this.data;
let threshold = contentHeight * 0.3; // 滑动阈值,超过30%高度则切换
threshold = Math.min(threshold, 200); // 最大阈值为200px
this.setData({
isTouching: false
});
// 触发微信原生事件
this.triggerEvent('change', {
current: this.data.currentIndex,
source: 'swipe'
});
// 根据滑动距离决定是否切换
if (Math.abs(touchDeltaY) > threshold) {
if (touchDeltaY > 0) {
// 向上滑动,切换到上一个,并标记来源为swipe
this.prev('swipe');
} else {
// 向下滑动,切换到下一个,并标记来源为swipe
this.next('swipe');
}
} else {
// 滑动距离不够,恢复原位置
this._updateContent();
// 恢复指示器位置
const tabbar = this._getTabbar();
if (tabbar && typeof tabbar.updateIndicator === 'function') {
tabbar.updateIndicator();
}
}
// 恢复自动播放
if (this.data.autoplay) {
this._startAutoplay();
}
},
/**
* 应用拖动效果
* @private
* @param {Number} deltaY 垂直位移
*/
_applyDragEffect(deltaY) {
// 在独立模式下应用拖动效果
if (this.data.contentsMode === 'independent') {
const contentNodes = this.getChildrenNodes('tabpanelContent');
const { currentIndex } = this.data;
if (!contentNodes || !contentNodes.length) return;
// 获取内容容器高度,用于计算基础位置
const query = this.createSelectorQuery();
query.select('.wv-vtabpanel-content-wrapper').boundingClientRect();
query.exec(res => {
if (!res || !res[0]) return;
const containerHeight = res[0].height;
// 应用变换到所有内容元素
contentNodes.forEach((content, index) => {
// 计算基础位置 - 相对于当前激活内容的位置
const basePosition = (index - currentIndex) * containerHeight;
// 加上拖动位移
const position = basePosition + deltaY;
// 应用无动画的变换 - 连续定位所有内容面板
const style = `transform: translateY(${position}px); transition-duration: 0ms; display: ${Math.abs(index - currentIndex) <= 1 ? 'block' : 'none'};`;
content.updateStyle(style);
});
});
}
},
}
});