wx-mini-weview
Version:
weixin UI
903 lines (772 loc) • 22.2 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) {
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;
}
}
});