UNPKG

wx-mini-weview

Version:

weixin UI

587 lines (510 loc) 15.9 kB
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); }); } } });