UNPKG

wx-mini-weview

Version:

weixin UI

903 lines (772 loc) 22.2 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) { this.linkChildNode('tabbarItems', target); this._buildNameIndexMaps(); this.updateChildren(); }, linkChanged() { this.updateChildren(); }, unlinked(target) { this.unlinkChildNode('tabbarItems', target); this._buildNameIndexMaps(); this.updateChildren(); } }, '../tabpanel/tabpanel': { type: 'parent', linked(target) { this.linkParentNode('tabpanel', target); }, linkChanged() { }, unlinked(target) { this.linkParentNode('tabpanel', target); } } }, behaviors: [ WvInheritable({ properties: {}, childrenProperties: { tabbarItems: { iconMode: { type: String, value: '' }, stretch: { type: Boolean, value: null }, width: { type: String, value: '' }, height: { type: String, value: '' }, theme: { type: String, value: 'primary' }, themeActive: { type: String, value: 'poppy' }, 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: null }, 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: '' }, 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: 'blink' // slide | blink | none }, indicatorAnimationDuration: { type: Number, value: 300 }, showIndicator: { type: Boolean, value: true }, indicatorTheme: { type: String, value: 'primary' }, indicatorPosition: { type: String, value: 'bottom' }, indicatorColor: { type: String, value: '' }, indicatorWidth: { type: String, value: '80%' }, indicatorHeight: { type: String, value: '6rpx' }, indicatorRadius: { type: String, value: '99rpx' }, gap: { type: String, value: 'var(--wv-margin-mini)' }, align: { type: String, value: 'left' }, noWrap:{ type: Boolean, value: true }, background: { type: String, value: 'rgba(var(--wv-theme-light-rgb), 1)' }, indicatorFollowSwipe: { type: Boolean, value: true }, }, data: { indicatorStyle: '', nameToIndexMap: {}, indexToNameMap: {}, _debounceupdateChildrenTimer: null, _debounceNameIndexMapsTimer: null, _prevIndicatorLeft: 0, _prevIndicatorWidth: 0, _animStopIndicator: false, _isShiftedIndicator: false }, lifetimes: { ready() { setTimeout(() => { this.updateChildren(() => { this.updateIndicator(); this.scrollToActiveTab(); this.triggerEvent('ready', { activeTab: this._getActiveTabInfo() }); }); }, 100); } }, 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 }); this._handleNavigation(targetChild); this.triggerEvent('change', { targetIndex: targetIndex, targetName: name, targeItem: targetChild }); return true; }, /** * 更新指示器位置和样式 */ updateIndicator() { const children = this.getChildrenNodes('tabbarItems'); if (!children || !children.length) return; const activeChild = this._findActiveChild(); if (!activeChild) return; const tabbarQuery = this.createSelectorQuery(); tabbarQuery.select('.wv-tabbar-content').boundingClientRect(); tabbarQuery.exec(tabbarRects => { if (!tabbarRects || !tabbarRects[0]) return; const tabbarRect = tabbarRects[0]; const containerLeft = tabbarRect.left; activeChild.getRect().then(rect => { if (!rect) return; const width = rect.width; const left = rect.left; const relativeLeft = left - containerLeft; const relativeCenter = relativeLeft + (width / 2); let indicatorWidth = this.data.indicatorWidth || '100%'; let indicatorStyle = ''; let currentLeft; let currentWidth; // 计算指示器的宽度和位置, 由于data.indicatorWidth单位允许%|rpx|px if (indicatorWidth.includes('%')) { const percentage = parseFloat(indicatorWidth) / 100; const actualWidth = width * percentage; const offset = (width - actualWidth) / 2; currentLeft = `${relativeLeft + offset}px`; currentWidth = `${actualWidth}px`; } else { indicatorStyle += `margin-left: calc(${width/2}px - ${indicatorWidth}/2);`; currentLeft = `${relativeLeft}px`; currentWidth = indicatorWidth; } if (this.data._isShiftedIndicator) { this._getCurrentIndicatorPosition().then(currentPos => { indicatorStyle += `--wv-indicator-left-from: ${currentPos.left}px; `; indicatorStyle += `--wv-indicator-width-from: ${currentPos.width}; `; indicatorStyle += `--wv-indicator-left: ${currentLeft}; `; indicatorStyle += `--wv-indicator-width: ${currentWidth};`; this.setData({ _animStopIndicator: true, _isShiftedIndicator: false }, () => { setTimeout(() => { this.setData({ indicatorStyle, _prevIndicatorLeft: currentLeft, _prevIndicatorWidth: currentWidth, _animStopIndicator: false }); }, 50); }); }); } else { indicatorStyle += `--wv-indicator-left-from: ${this.data._prevIndicatorLeft}; `; indicatorStyle += `--wv-indicator-width-from: ${this.data._prevIndicatorWidth}; `; indicatorStyle += `--wv-indicator-left: ${currentLeft}; `; indicatorStyle += `--wv-indicator-width: ${currentWidth};`; this.setData({ _animStopIndicator: true, }, () => { setTimeout(() => { this.setData({ indicatorStyle, _prevIndicatorLeft: currentLeft, _prevIndicatorWidth: currentWidth, _animStopIndicator: false }); }, 50); }); } }); }); }, /** * 获取指示器当前位置 * @return {Promise} 包含left和width的Promise * @private */ _getCurrentIndicatorPosition() { return new Promise((resolve) => { const position = this.data.indicatorPosition; const query = this.createSelectorQuery(); query.select(`.wv-tabbar-indicator.${position} .indicator`).boundingClientRect(); query.select('.wv-tabbar-content').boundingClientRect(); query.exec(res => { if (res && res[0] && res[1]) { const indicatorRect = res[0]; const containerRect = res[1]; const relativeLeft = indicatorRect.left - containerRect.left; resolve({ left: relativeLeft, width: `${indicatorRect.width}px` }); } else { resolve({ left: parseFloat(this.data._prevIndicatorLeft), width: this.data._prevIndicatorWidth }); } }); }); }, /** * 滑动过程中移动指示器 * @param {String} direction - 移动方向 'left' 或 'right' * @param {Number} percent - 移动百分比 (0-100) */ shiftIndicator(direction, percent) { if (!this.data.indicatorFollowSwipe) return; if (direction !== 'left' && direction !== 'right') 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 === 'left') { 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-tabbar-content').boundingClientRect(); tabbarQuery.exec(tabbarRects => { if (!tabbarRects || !tabbarRects[0]) return; const containerLeft = tabbarRects[0].left; const currentLeft = currentRect.left - containerLeft; const currentWidth = currentRect.width; const targetLeft = targetRect.left - containerLeft; const targetWidth = targetRect.width; const factor = validPercent / 100; const interpolatedLeft = currentLeft + (targetLeft - currentLeft) * factor; const interpolatedWidth = currentWidth + (targetWidth - currentWidth) * factor; let indicatorStyle = ''; let indicatorWidth = this.data.indicatorWidth || '100%'; if (indicatorWidth.includes('%')) { const percentage = parseFloat(indicatorWidth) / 100; const actualWidth = interpolatedWidth * percentage; const offset = (interpolatedWidth - actualWidth) / 2; indicatorStyle += `--wv-indicator-left: ${interpolatedLeft + offset}px; `; indicatorStyle += `--wv-indicator-width: ${actualWidth}px;`; } else { indicatorStyle += `--wv-indicator-left: ${interpolatedLeft}px; `; indicatorStyle += `margin-left: calc(${interpolatedWidth/2}px - ${indicatorWidth}/2);`; } this.setData({ indicatorStyle, _isShiftedIndicator: true // Mark the indicator as shifted }); }); }); }, scrollToActiveTab() { const targetChild = this._findActiveChild(); if (!targetChild) return; setTimeout(() => { const query = this.createSelectorQuery(); query.select('.wv-tabbar').boundingClientRect(); targetChild.getRect().then(rect => { if (!rect) return; query.exec(res => { if (!res || !res[0]) return; const tabbarRect = res[0]; const tabbarWidth = tabbarRect.width; const scrollQuery = this.createSelectorQuery(); scrollQuery.select('.wv-tabbar').scrollOffset().exec(scrollRes => { if (!scrollRes || !scrollRes[0]) return; const { scrollLeft, scrollWidth } = scrollRes[0]; const targetLeft = rect.left - tabbarRect.left + scrollLeft; const targetCenter = targetLeft + (rect.width / 2); let newScrollLeft = targetCenter - tabbarWidth / 2; newScrollLeft = Math.max(0, Math.min(newScrollLeft, scrollWidth - tabbarWidth)); this.setData({ tabbarScrollLeft: newScrollLeft }); }); }); }); }, 50); }, /** * 设置激活的标签 * @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; }, /** * 获取当前激活的标签信息 * @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; } } });