wx-mini-weview
Version:
weixin UI
587 lines (510 loc) • 15.9 kB
JavaScript
const { WvInheritable } = require('../../behaviors/WvInheritable');
const { WvComponentEvent } = require('../../behaviors/WvComponentEvent');
Component({
options: {
styleIsolation: 'apply-shared',
multipleSlots: true
},
relations: {
'../tabbar/tabbar': {
type: 'child',
linked(target) {
this.linkChildNode('tabbar', target);
this._initTabbar();
},
linkChanged() {
},
unlinked(target) {
this.unlinkChildNode('tabbar', target);
this._resetTabbar();
}
},
'../tabpanel-content/tabpanel-content': {
type: 'child',
linked(target) {
this.linkChildNode('tabpanelContent', target);
this._initContent();
},
linkChanged() {
},
unlinked(target) {
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'
},
// 整个 TabPanel 的高度
height: {
type: String,
value: '300rpx'
},
// TabBar 的位置,可以是 'top' 或 'bottom'
tabbarPosition: {
type: String,
value: 'top'
},
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'
},
contentSwipeable: {
type: Boolean,
value: true
},
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
},
// contentHeight为空的时候,表示随flex布局自然而然成为height-tabbar部分的高度也就是flex: 1的结果,如果指定了,则拥有自己的高度
contentHeight: {
type: String,
value: ''
},
// 自定义类名
customClass: {
type: String,
value: ''
}
},
data: {
currentIndex: 0,
contentItemCount: 0,
touchStartX: 0,
touchStartY: 0,
touchDeltaX: 0,
isTouching: false,
isAnimating: false,
contentWidth: -1,
autoplayTimer: null,
// 添加防抖计时器
_debounceInitContentTimer: null,
_debounceUpdatePositionsTimer: null,
_debounceSetActiveTimer: null
},
lifetimes: {
attached() {
},
ready() {
this._getContentWidth();
this.refresh();
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;
this.setData({
contentItemCount: contentNodes.length
});
}, 50);
},
/**
* 初始化标签栏组件
* @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) {
// 标记事件来源为tabbar,避免循环同步
this.setActive(detail.targetIndex, 'tabbar');
}
}, 'tabpanel');
},
/**
* 清理标签栏事件监听
* @private
*/
_resetTabbar() {
const tabbar = this._getTabbar();
if (tabbar) {
// 取消监听
tabbar.off('change', 'tabpanel');
}
},
/**
* 获取标签栏组件实例
* @private
* @return {Object|null} 标签栏组件实例
*/
_getTabbar() {
const tabbarNodes = this.getChildrenNodes('tabbar');
return tabbarNodes && tabbarNodes.length > 0 ? tabbarNodes[0] : null;
},
/**
* 刷新面板布局
* 当TabPanel位于初始隐藏的容器(如SlidePanel)中时,
* 容器显示后需要调用此方法重新计算和更新内容布局
* @public
*/
refresh() {
const that = this
this.refreshInterval = setInterval(() => {
if (this.data.contentWidth == -1) {
this._getContentWidth();
}else{
clearInterval(that.refreshInterval);
}
}, 200)
},
/**
* 获取内容区域的宽度
* @private
*/
_getContentWidth() {
const query = this.createSelectorQuery();
query.select('.wv-tabpanel-container').boundingClientRect();
query.exec(res => {
if (res && res[0]) {
if(res[0].width > 0){
this.setData({
contentWidth: res[0].width
}, () => {
this._updateContentPositions();
if (this.data.active !== undefined) {
this.setActive(this.data.active, 'refresh');
}
});
}
}
});
},
/**
* 更新内容项位置
* @private
*/
_updateContentPositions() {
// 添加防抖
if (this.data._debounceUpdatePositionsTimer) {
clearTimeout(this.data._debounceUpdatePositionsTimer);
}
this.data._debounceUpdatePositionsTimer = setTimeout(() => {
const { currentIndex, contentWidth, contentItemCount } = this.data;
const contentNodes = this.getChildrenNodes('tabpanelContent');
if (!contentNodes || !contentNodes.length || contentWidth === 0) return;
contentNodes.forEach((content, index) => {
// 计算位置,设置transform样式
const position = (index - currentIndex) * contentWidth;
const style = `transform: translateX(${position}px); transition-duration: ${this.data.isAnimating ? this.data.duration : 0}ms;`;
// 更新激活状态和位置样式
content.setActive(index === currentIndex, index);
content.updateStyle(style);
});
}, 10);
},
/**
* 切换到指定索引的内容
* @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._updateContentPositions();
// 只有当事件不是来自tabbar时,才需要同步回tabbar
if (source !== 'tabbar') {
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) {
if (this.data.autoplayTimer) {
clearInterval(this.data.autoplayTimer);
}
const timer = setInterval(() => {
if (!this.data.isTouching) {
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) return;
const touch = e.touches[0];
this.setData({
touchStartX: touch.clientX,
touchStartY: touch.clientY,
touchDeltaX: 0,
isTouching: true
});
// 暂停自动播放
if (this.data.autoplay) {
this._stopAutoplay();
}
},
/**
* 触摸移动事件处理
* @param {Object} e 事件对象
*/
onTouchMove(e) {
if (!this.data.contentSwipeable || !this.data.isTouching) return;
const touch = e.touches[0];
const deltaX = touch.clientX - this.data.touchStartX;
const deltaY = touch.clientY - this.data.touchStartY;
// 如果是纵向滑动,不进行横向滑动处理
if (Math.abs(deltaY) > Math.abs(deltaX)) {
return;
}
// 边界检查:如果不是循环模式,在首尾项限制滑动
// if (!this.data.loop) {
// if ((this.data.currentIndex === 0 && deltaX > 0) ||
// (this.data.currentIndex === this.data.contentItemCount - 1 && deltaX < 0)) {
// return;
// }
// }
this.setData({
touchDeltaX: deltaX
});
// 计算滑动百分比
const absPercentage = Math.min(Math.abs(deltaX) / this.data.contentWidth * 100, 100);
// 更新内容位置,应用拖动效果
this._applyDragEffect(deltaX);
// 计算指示器移动方向
const direction = deltaX < 0 ? 'right' : 'left';
// 调用tabbar的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) return;
const { touchDeltaX, contentWidth } = this.data;
let threshold = contentWidth * 0.3; // 滑动阈值,超过30%宽度则切换
threshold = Math.min(threshold, 120); // 最大阈值为120px
this.setData({
isTouching: false
});
// 触发微信原生事件
this.triggerEvent('change', {
current: this.data.currentIndex,
source: 'swipe'
});
// 根据滑动距离决定是否切换
if (Math.abs(touchDeltaX) > threshold) {
if (touchDeltaX > 0) {
// 向右滑动,切换到上一个,并标记来源为swipe
this.prev('swipe');
} else {
// 向左滑动,切换到下一个,并标记来源为swipe
this.next('swipe');
}
} else {
// 滑动距离不够,恢复原位置
this._updateContentPositions();
// 恢复指示器位置
const tabbar = this._getTabbar();
if (tabbar && typeof tabbar.updateIndicator === 'function') {
tabbar.updateIndicator();
}
}
// 恢复自动播放
if (this.data.autoplay) {
this._startAutoplay();
}
},
/**
* 应用拖动效果
* @private
* @param {Number} deltaX 水平位移
*/
_applyDragEffect(deltaX) {
const contentNodes = this.getChildrenNodes('tabpanelContent');
const { currentIndex, contentWidth } = this.data;
if (!contentNodes || !contentNodes.length) return;
contentNodes.forEach((content, index) => {
// 计算基础位置
const basePosition = (index - currentIndex) * contentWidth;
// 加上拖动位移
const position = basePosition + deltaX;
// 应用无动画的变换
const style = `transform: translateX(${position}px); transition-duration: 0ms;`;
content.updateStyle(style);
});
}
}
});