UNPKG

wx-mini-weview

Version:

weixin UI

983 lines (848 loc) 25.4 kB
const { WvInheritable } = require('../../behaviors/WvInheritable'); const { WvComponentEvent } = require('../../behaviors/WvComponentEvent'); Component({ options: { styleIsolation: 'apply-shared' }, relations: { '../tabbar-item/tabbar-item': { type: 'child', linked(target) { // 使用WvInheritable的方法注册子节点 this.linkChildNode('tabbarItems', target); // 构建名称和索引的映射 - 当有新的tab项添加时重建映射 this._buildNameIndexMaps(); this.updateChildren(); }, linkChanged() { this.updateChildren(); }, unlinked(target) { // 使用WvInheritable的方法注销子节点 this.unlinkChildNode('tabbarItems', target); // 构建名称和索引的映射 - 当有tab项移除时重建映射 this._buildNameIndexMaps(); this.updateChildren(); } }, '../vtabpanel/vtabpanel': { type: 'parent', linked(target) { this.linkParentNode('vtabpanel', target); }, linkChanged() { }, unlinked(target) { this.linkParentNode('vtabpanel', target); } } }, behaviors: [ WvInheritable({ properties: {}, childrenProperties: { tabbarItems: { iconMode: { type: String, value: '' }, align: { type: String, value: 'center' }, stretch: { type: Boolean, value: false }, theme: { type: String, value: '' }, themeActive: { type: String, value: '' }, color: { type: String, value: '' }, fontSize: { type: String, value: '' }, colorActive: { type: String, value: '' }, fontSizeActive: { type: String, value: '' }, bold: { type: Boolean, value: null }, boldActive: { type: Boolean, value: null }, background: { type: String, value: '' }, backgroundActive: { type: String, value: '' }, scaleActive: { type: Number, value: 1 }, // 文本样式 textColor: { type: String, value: '' }, textFontSize: { type: String, value: '' }, textColorActive: { type: String, value: '' }, textFontSizeActive: { type: String, value: '' }, // 图标样式 iconSize: { type: String, value: '' }, iconColor: { type: String, value: '' }, iconColorActive: { type: String, value: '' }, iconFontSizeActive: { type: String, value: '' }, padding: { type: String, value: 'var(--wv-padding-default) var(--wv-padding-default)' }, radius: { type: String, value: '' }, openType: { type: String, value: '' }, arcSize: { type: String, value: '0' } } } }), WvComponentEvent ], properties: { active: { type: null, // 可以是索引数字或名称字符串 value: 0, observer(newVal, oldVal) { if (newVal !== oldVal) { this.updateChildren(() => { this.updateIndicator(); this.scrollToActiveTab(); }); } } }, indicatorAnimation: { type: String, value: 'slide' // slide | blink | none }, indicatorAnimationDuration: { type: Number, value: 300 }, // 是否显示指示器 showIndicator: { type: Boolean, value: true }, // 指示器主题 indicatorTheme: { type: String, value: 'primary' }, // 指示器位置,'left' 或 'right'(垂直布局) indicatorPosition: { type: String, value: 'right' }, // 指示器颜色(优先于主题) indicatorColor: { type: String, value: '' }, // 指示器宽度,如'4rpx', '6px'等(实际是宽度) indicatorWidth: { type: String, value: '6rpx' }, // 指示器高度,如'60%', '30px'等(实际是高度的百分比) indicatorHeight: { type: String, value: '80%' }, // 指示器圆角半径 indicatorRadius: { type: String, value: '99rpx' }, // 项目间隙 gap: { type: String, value: 'var(--wv-margin-mini)' }, height: { type: String, value: '100%' }, background: { type: String, value: 'rgba(var(--wv-theme-light-rgb), 1)' }, }, data: { indicatorStyle: '', animationDuration: 200, nameToIndexMap: {}, indexToNameMap: {}, tabbarScrollTop: 0, _debounceupdateChildrenTimer: null, _debounceNameIndexMapsTimer: null, _debounceUpdateIndicatorTimer: null, _debounceScrollToActiveTabTimer: null, // Add animation state tracking _prevIndicatorTop: 0, _prevIndicatorHeight: 0, _animStopIndicator: false, _isShiftedIndicator: false // Add this flag to track indicator shift state }, lifetimes: { ready() { setTimeout(() => { this.updateChildren(() => { this.updateIndicator(); this.scrollToActiveTab(); // 触发初始化完成事件 this.triggerEvent('ready', { activeTab: this._getActiveTabInfo() }); // triggerEvent会自动通过WvComponentEvent发出事件 }); }, 100); } }, methods: { // Core Methods /** * 更新子组件状态 */ updateChildren(calback) { if(this.data._debounceupdateChildrenTimer) { clearTimeout(this.data._debounceupdateChildrenTimer); } this.data._debounceupdateChildrenTimer = setTimeout(() => { const children = this.getChildrenNodes('tabbarItems'); if (!children || !children.length) return; // 获取目标索引 const targetIndex = this._getTargetIndex(this.data.active); // 在一个循环中同时设置激活和非激活状态 children.forEach((child, index) => { const isActive = index === targetIndex; if (isActive && !child.setActive(true)) { console.warn('Target tab is disabled or does not exist:', this.data.active); } else if (!isActive) { child.setActive(false); } }); if (calback) { calback(); } }, 30); }, /** * 设置激活的标签 * @param {String} name - 标签名称 * @param {Number} index - 标签索引 * @return {Boolean} 是否设置成功 */ setActive(name, index) { const currentActive = this.data.active; if ((typeof currentActive === 'string' && name === currentActive) || (typeof currentActive === 'number' && index === currentActive)) { return false; } const targetData = this._getTargetTabData(name, index); if (!targetData) return false; const { targetChild, targetIndex } = targetData; if (targetChild.data.wvInheritableApply.disabled) return false; const detail = { index: targetIndex, name: name, current: this.data.active }; const beforeChangeEvent = this.triggerEvent('beforeChange', detail, { bubbles: true, composed: true }); if (beforeChangeEvent && beforeChangeEvent.defaultPrevented) { return false; } // 设置激活状态 const newActive = name || targetIndex; this.setData({ active: newActive }); // 执行URL导航逻辑 this._handleNavigation(targetChild); this.triggerEvent('change', { targetIndex: targetIndex, targetName: name, targeItem: targetChild }); // WvComponentEvent 会自动发出 change 事件 return true; }, /** * 更新指示器位置和样式 */ updateIndicator() { if (this.data._debounceUpdateIndicatorTimer) { clearTimeout(this.data._debounceUpdateIndicatorTimer); } this.data._debounceUpdateIndicatorTimer = setTimeout(() => { const children = this.getChildrenNodes('tabbarItems'); if (!children || !children.length) return; // 查找激活的子组件 const activeChild = this._findActiveChild(); if (!activeChild) return; // 获取TabBar容器和激活项的位置信息以计算指示器位置 const tabbarQuery = this.createSelectorQuery(); tabbarQuery.select('.wv-vtabbar-content').boundingClientRect(); tabbarQuery.exec(tabbarRects => { if (!tabbarRects || !tabbarRects[0]) return; const tabbarRect = tabbarRects[0]; const containerTop = tabbarRect.top; activeChild.getRect().then(rect => { if (!rect) return; const height = rect.height; const top = rect.top; const relativeTop = top - containerTop; let indicatorHeight = this.data.indicatorHeight || '100%'; // 构建指示器样式(针对垂直方向) let indicatorStyle = ''; let currentTop; let currentHeight; if (indicatorHeight.includes('%')) { const percentage = parseFloat(indicatorHeight) / 100; const actualHeight = height * percentage; const offset = (height - actualHeight) / 2; currentTop = `${relativeTop + offset}px`; currentHeight = `${actualHeight}px`; } else { indicatorStyle += `transform: translateY(calc(50% - ${this.data.indicatorHeight}/2));`; currentTop = `${relativeTop}px`; currentHeight = indicatorHeight; } if (this.data._isShiftedIndicator) { this._getCurrentIndicatorPosition().then(currentPos => { indicatorStyle += `--wv-indicator-top-from: ${currentPos.top}px; `; indicatorStyle += `--wv-indicator-height-from: ${currentPos.height}; `; indicatorStyle += `--wv-indicator-top: ${currentTop}; `; indicatorStyle += `--wv-indicator-height: ${currentHeight};`; this.setData({ _animStopIndicator: true, _isShiftedIndicator: false }, () => { setTimeout(() => { this.setData({ indicatorStyle, _prevIndicatorTop: currentTop, _prevIndicatorHeight: currentHeight, _animStopIndicator: false }); }, 50) }); }); } else { indicatorStyle += `--wv-indicator-top-from: ${this.data._prevIndicatorTop}; `; indicatorStyle += `--wv-indicator-height-from: ${this.data._prevIndicatorHeight}; `; indicatorStyle += `--wv-indicator-top: ${currentTop}; `; indicatorStyle += `--wv-indicator-height: ${currentHeight};`; this.setData({ _animStopIndicator: true, }, () => { setTimeout(() => { this.setData({ indicatorStyle, _prevIndicatorTop: currentTop, _prevIndicatorHeight: currentHeight, _animStopIndicator: false }); }, 50) }); } }); }); }, 30); }, /** * 获取指示器当前位置 * @return {Promise} 包含top和height的Promise * @private */ _getCurrentIndicatorPosition() { return new Promise((resolve) => { // 选择器应该针对当前位置的indicator const position = this.data.indicatorPosition; const query = this.createSelectorQuery(); // 获取指示器和容器的位置信息 query.select(`.wv-vtabbar-indicator.${position} .indicator`).boundingClientRect(); query.select('.wv-vtabbar-content').boundingClientRect(); query.exec(res => { if (res && res[0] && res[1]) { const indicatorRect = res[0]; const containerRect = res[1]; const relativeTop = indicatorRect.top - containerRect.top; resolve({ top: relativeTop, height: `${indicatorRect.height}px` }); } else { resolve({ top: parseFloat(this.data._prevIndicatorTop), height: this.data._prevIndicatorHeight }); } }); }); }, /** * 滑动过程中移动指示器 * @param {String} direction - 移动方向 'up' 或 'down' * @param {Number} percent - 移动百分比 (0-100) */ shiftIndicator(direction, percent) { if (!this.data.showIndicator) return; if (direction !== 'up' && direction !== 'down') return; // 确保百分比在有效范围内 const validPercent = Math.max(0, Math.min(100, percent)); // 获取子节点和当前激活索引 const children = this.getChildrenNodes('tabbarItems'); if (!children || !children.length) return; const currentIndex = this._getCurrentIndex(); let targetIndex; // 根据方向确定目标索引 if (direction === 'up') { targetIndex = Math.max(0, currentIndex - 1); } else { targetIndex = Math.min(children.length - 1, currentIndex + 1); } // 如果已经在边界,则不继续移动 if (targetIndex === currentIndex) return; // 获取当前标签和目标标签 const currentTab = children[currentIndex]; const targetTab = children[targetIndex]; if (!currentTab || !targetTab) return; // 获取两个标签的尺寸信息 Promise.all([ currentTab.getRect(), targetTab.getRect() ]).then(([currentRect, targetRect]) => { if (!currentRect || !targetRect) return; const tabbarQuery = this.createSelectorQuery(); tabbarQuery.select('.wv-vtabbar-content').boundingClientRect(); tabbarQuery.exec(tabbarRects => { if (!tabbarRects || !tabbarRects[0]) return; const containerTop = tabbarRects[0].top; // 计算位置和高度 const currentTop = currentRect.top - containerTop; const currentHeight = currentRect.height; const targetTop = targetRect.top - containerTop; const targetHeight = targetRect.height; // 根据百分比插值计算 const factor = validPercent / 100; const interpolatedTop = currentTop + (targetTop - currentTop) * factor; const interpolatedHeight = currentHeight + (targetHeight - currentHeight) * factor; // 创建指示器样式 let indicatorStyle = ''; let indicatorHeight = this.data.indicatorHeight || '80%'; // 根据指示器高度类型应用不同的样式 if (indicatorHeight.includes('%')) { const percentage = parseFloat(indicatorHeight) / 100; const actualHeight = interpolatedHeight * percentage; const offset = (interpolatedHeight - actualHeight) / 2; indicatorStyle += `--wv-indicator-top: ${interpolatedTop + offset}px; `; indicatorStyle += `--wv-indicator-height: ${actualHeight}px;`; } else { indicatorStyle += `--wv-indicator-top: ${interpolatedTop}px; `; indicatorStyle += `transform: translateY(calc(50% - ${indicatorHeight}/2));`; } // 更新指示器样式 this.setData({ indicatorStyle, _isShiftedIndicator: true // Mark the indicator as shifted }); }); }); }, /** * 滚动到激活的标签 */ scrollToActiveTab() { if (this.data._debounceScrollToActiveTabTimer) { clearTimeout(this.data._debounceScrollToActiveTabTimer); } this.data._debounceScrollToActiveTabTimer = setTimeout(() => { const targetChild = this._findActiveChild(); if (!targetChild) return; // 获取 tabbar 容器和目标元素的位置信息 const query = this.createSelectorQuery(); query.select('.wv-vtabbar').boundingClientRect(); targetChild.getRect().then(rect => { if (!rect) return; query.exec(res => { if (!res || !res[0]) return; const tabbarRect = res[0]; const tabbarHeight = tabbarRect.height; // 获取 tabbar 的当前滚动位置 const scrollQuery = this.createSelectorQuery(); scrollQuery.select('.wv-vtabbar').scrollOffset().exec(scrollRes => { if (!scrollRes || !scrollRes[0]) return; const { scrollTop, scrollHeight } = scrollRes[0]; // 计算目标元素相对于容器的位置和中心点 const targetTop = rect.top - tabbarRect.top + scrollTop; const targetCenter = targetTop + (rect.height / 2); // 计算容器的中心点 const containerCenter = tabbarHeight / 2; // 计算需要滚动的距离并处理边界情况 let newScrollTop = targetCenter - containerCenter; newScrollTop = Math.max(0, Math.min(newScrollTop, scrollHeight - tabbarHeight)); this.setData({ tabbarScrollTop: newScrollTop }); }); }); }); }, 30); }, // Public API Methods /** * 设置激活的标签 * @param {String|Number} nameOrIndex - 标签名称或索引 * @return {Boolean} 是否设置成功 */ setActiveTab(nameOrIndex) { if (typeof nameOrIndex === 'string') { return this.setActive(nameOrIndex); } else if (typeof nameOrIndex === 'number') { return this.setActive(null, nameOrIndex); } return false; }, /** * 设置激活的标签(静默模式,不触发回调) * @param {String|Number} nameOrIndex - 标签名称或索引 * @return {Boolean} 是否设置成功 */ setActiveTabSilent(nameOrIndex) { if (typeof nameOrIndex === 'string') { const index = this.data.nameToIndexMap[nameOrIndex]; if (index === undefined) return false; this.setData({ active: nameOrIndex }); } else if (typeof nameOrIndex === 'number') { if (nameOrIndex < 0 || nameOrIndex >= this.getChildrenNodes('tabbarItems').length) { return false; } this.setData({ active: nameOrIndex }); } else { return false; } // 更新子组件状态 this.updateChildren(() => { // 确保指示器和滚动位置也更新 this.updateIndicator(); this.scrollToActiveTab(); }); return true; }, /** * 获取当前激活的标签信息 * @return {Object} 包含激活标签信息的对象 */ getActiveTab() { return this._getActiveTabInfo(); }, /** * 获取所有标签信息 * @return {Array} 所有标签信息的数组 */ getAllTabs() { const children = this.getChildrenNodes('tabbarItems'); if (!children || !children.length) return []; return children.map((child, index) => ({ index: index, name: child.data.wvInheritableApply.name, text: child.data.wvInheritableApply.text, disabled: child.data.wvInheritableApply.disabled, active: child.data.wvInheritableApply.active })); }, /** * 禁用指定标签 * @param {String|Number} nameOrIndex - 标签名称或索引 * @return {Boolean} 是否设置成功 */ disableTab(nameOrIndex) { return this._setTabDisabledState(nameOrIndex, true); }, /** * 启用指定标签 * @param {String|Number} nameOrIndex - 标签名称或索引 * @return {Boolean} 是否设置成功 */ enableTab(nameOrIndex) { return this._setTabDisabledState(nameOrIndex, false); }, /** * 切换到下一个可用的标签 * @return {Boolean} 是否切换成功 */ next() { const currentIndex = this._getCurrentIndex(); const index = this._findEnabledTab(currentIndex, 1); return index !== null ? this.setActive(null, index) : false; }, /** * 切换到上一个可用的标签 * @return {Boolean} 是否切换成功 */ previous() { const currentIndex = this._getCurrentIndex(); const index = this._findEnabledTab(currentIndex, -1); return index !== null ? this.setActive(null, index) : false; }, /** * 切换到第一个可用的标签 * @return {Boolean} 是否切换成功 */ first() { const index = this._findEnabledTab(-1, 1); return index !== null ? this.setActive(null, index) : false; }, /** * 切换到最后一个可用的标签 * @return {Boolean} 是否切换成功 */ last() { const children = this.getChildrenNodes('tabbarItems'); if (!children || !children.length) return false; const index = this._findEnabledTab(children.length, -1); return index !== null ? this.setActive(null, index) : false; }, // Private Helper Methods /** * 构建名称和索引的映射 * @private */ _buildNameIndexMaps() { if (this.data._debounceNameIndexMapsTimer) { clearTimeout(this.data._debounceNameIndexMapsTimer); } this.data._debounceNameIndexMapsTimer = setTimeout(() => { const children = this.getChildrenNodes('tabbarItems'); if (!children || !children.length) return; const nameToIndexMap = {}; const indexToNameMap = {}; children.forEach((child, index) => { const name = child.data.wvInheritableApply.name || `index_${index}`; nameToIndexMap[name] = index; indexToNameMap[index] = name; }); this.setData({ nameToIndexMap, indexToNameMap }); }, 30); }, /** * 获取目标索引 * @param {String|Number} active - 激活项标识 * @return {Number} 目标索引 * @private */ _getTargetIndex(active) { return typeof active === 'number' ? active : this.data.nameToIndexMap[active]; }, /** * 获取目标标签数据 * @param {String} name - 标签名称 * @param {Number} index - 标签索引 * @return {Object|null} 目标标签数据 * @private */ _getTargetTabData(name, index) { const children = this.getChildrenNodes('tabbarItems'); if (!children || !children.length) return null; let targetChild, targetIndex; if (typeof name === 'string') { targetIndex = this.data.nameToIndexMap[name]; targetChild = targetIndex !== undefined ? children[targetIndex] : null; } else { targetIndex = index; targetChild = index >= 0 && index < children.length ? children[index] : null; } return targetChild ? { targetChild, targetIndex } : null; }, /** * 处理导航逻辑 * @param {Object} targetChild - 目标子组件 * @private */ _handleNavigation(targetChild) { const url = targetChild.data.wvInheritableApply.url; if (!url) return; const openType = targetChild.data.wvInheritableApply.openType || 'redirectTo'; const navigationFunctions = { switchTab: () => { wx.switchTab({ url: url, success: () => { const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; const tabBar = currentPage.getTabBar?.(); if (tabBar) { tabBar.setData({ active: this.data.active }); } }, fail: (err) => console.error('switchTab导航失败', err) }); }, redirectTo: () => { wx.redirectTo({ url: url, fail: (res) => console.error('redirectTo导航失败', res) }); }, navigateTo: () => { wx.navigateTo({ url: url, fail: (res) => console.error('navigateTo导航失败', res) }); }, reLaunch: () => { wx.reLaunch({ url: url, fail: (res) => console.error('reLaunch导航失败', res) }); } }; navigationFunctions[openType]?.(); }, /** * 设置标签禁用状态 * @param {String|Number} nameOrIndex - 标签名称或索引 * @param {Boolean} disabled - 是否禁用 * @return {Boolean} 是否设置成功 * @private */ _setTabDisabledState(nameOrIndex, disabled) { const targetData = typeof nameOrIndex === 'string' ? this._getTargetTabData(nameOrIndex) : this._getTargetTabData(null, nameOrIndex); if (targetData) { targetData.targetChild.setData({ disabled }); return true; } return false; }, /** * 查找可用的标签 * @param {Number} startIndex - 开始查找的索引 * @param {Number} step - 索引增加的步长 (1 或 -1) * @return {Number|null} 找到的索引或null * @private */ _findEnabledTab(startIndex, step) { const children = this.getChildrenNodes('tabbarItems'); if (!children || !children.length) return null; let index = startIndex; let loopCount = 0; const len = children.length; while (loopCount < len) { index = (index + step + len) % len; if (!children[index].data.wvInheritableApply.disabled) { return index; } loopCount++; } return null; }, /** * 获取当前激活的索引 * @return {Number} 当前激活的索引 * @private */ _getCurrentIndex() { if (typeof this.data.active === 'number') { return this.data.active; } return this.data.nameToIndexMap[this.data.active] || 0; }, /** * 查找激活的子组件 * @return {Object|null} 激活的子组件 * @private */ _findActiveChild() { const children = this.getChildrenNodes('tabbarItems'); if (!children || !children.length) return null; let activeChild = null; for (let i = 0; i < children.length; i++) { if (children[i].data.active === true) { activeChild = children[i]; break; } } return activeChild; }, /** * 获取激活标签信息 * @return {Object|null} 激活的标签信息 * @private */ _getActiveTabInfo() { const children = this.getChildrenNodes('tabbarItems'); if (!children || !children.length) return null; let activeChild = null; let activeIndex = -1; children.forEach((child, index) => { if (child.data.wvInheritableApply.active === true) { activeChild = child; activeIndex = index; } }); if (activeChild) { return { index: activeIndex, name: activeChild.data.wvInheritableApply.name, text: activeChild.data.wvInheritableApply.text, disabled: activeChild.data.wvInheritableApply.disabled }; } return null; } } });