wx-mini-weview
Version:
weixin UI
983 lines (848 loc) • 25.4 kB
JavaScript
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;
}
}
});