refactor(utils&tabbar): 重构tabbar高亮逻辑与分包路径适配

1. 修复store中tabBarList初始值为null替代空字符串
2. 重构adaptSubpackageUrl,优化正则匹配与移除冗余映射
3. 新增路径归一化、分包名提取工具函数
4. 重写checkTabBarActive,新增精确匹配、packageEntryTab优先级匹配逻辑
5. 重构diy-bottom-nav组件的路由更新与高亮校验逻辑,统一路径处理流程
This commit is contained in:
2026-06-16 14:56:17 +08:00
parent c0e64437f0
commit 92700f9a21
3 changed files with 166 additions and 158 deletions

View File

@@ -256,13 +256,11 @@ export const adaptSubpackageUrl = (url) => {
* 系统当前的支持tarbarList(包含动态tabBar),注意这是实际分包后的路径 * 系统当前的支持tarbarList(包含动态tabBar),注意这是实际分包后的路径
* 根据匹配映射关系,修改url为实际分包后的路径 * 根据匹配映射关系,修改url为实际分包后的路径
*/ */
// 定义前缀映射表 // 定义前缀映射表(key=旧前缀, value=新分包前缀)
const urlPrefixMap = { const urlPrefixMap = {
'/pages/promotion/': '/pages_promotion/', '/pages/promotion/': '/pages_promotion/',
'/pages/order/': '/pages_order/', '/pages/order/': '/pages_order/',
'/pages/goods/': '/pages_goods/', '/pages/goods/': '/pages_goods/',
'/pages/contact/': '/pages_tool/contact/',
'/pages/member/': '/pages_tool/member/', '/pages/member/': '/pages_tool/member/',
'/pages/login/': '/pages_tool/login/', '/pages/login/': '/pages_tool/login/',
'/pages/agreement/': '/pages_tool/agreement/', '/pages/agreement/': '/pages_tool/agreement/',
@@ -282,29 +280,67 @@ export const adaptSubpackageUrl = (url) => {
'/pages/webview/': '/pages_tool/webview/', '/pages/webview/': '/pages_tool/webview/',
}; };
// 构建正则表达式 // 构建正则表达式(加 ^ 锚定前缀,避免路径中间出现 /pages/xxx/ 时误匹配)
const prefixes = Object.keys(urlPrefixMap);
if (prefixes.length === 0) return url;
const regex = new RegExp( const regex = new RegExp(
Object.keys(urlPrefixMap).map(key => '^(' + prefixes.map(key =>
key.replace(/\//g, '\\/').replace(/\*/g, '.*') key.replace(/\//g, '\\/').replace(/\*/g, '.*')
).join('|'), ).join('|') + ')'
'g'
); );
// 替换函数 // 替换函数
function replacePrefix(str) { function replacePrefix(str) {
return str.replace(regex, match => { return str.replace(regex, match => {
for (const [oldPrefix, newPrefix] of Object.entries(urlPrefixMap)) { const newPrefix = urlPrefixMap[match];
if (match.startsWith(oldPrefix)) { return newPrefix || match;
return match.replace(oldPrefix, newPrefix);
}
}
return match; // 默认返回原匹配
}); });
} }
return replacePrefix(url); return replacePrefix(url);
} }
// 归一化任意路径:确保以 '/' 开头,去除 query,适配子包
function _normalizePath(path) {
if (!path) return '';
const normalized = path.startsWith('/') ? path : '/' + path;
return adaptSubpackageUrl(normalized).split('?')[0];
}
// 从归一化路径提取分包名(parts[1])
function _getPackageName(normalizedPath) {
const parts = normalizedPath.split('/');
return parts[1] || '';
}
// 给定当前页面路径 + tab 配置,返回当前页面所属的 tab URL
// 优先级:精确匹配 > packageEntryTab > 分包名回退匹配
function _findMatchingTabUrl(currentPagePath, tabBarList, packageEntryTab) {
if (!tabBarList || !tabBarList.list || !Array.isArray(tabBarList.list)) return null;
const tabUrls = tabBarList.list
.filter(item => item.link && item.link.wap_url)
.map(item => _normalizePath(item.link.wap_url));
const normalizedCurrent = _normalizePath(currentPagePath);
// 1) 精确匹配 → 当前页面就是 tab 页面
const exactMatch = tabUrls.find(url => url === normalizedCurrent);
if (exactMatch) return exactMatch;
// 2) 通过 packageEntryTab(由 updateCurrentRoute/redirectTo 写入)
const packageName = _getPackageName(normalizedCurrent);
if (packageEntryTab && typeof packageEntryTab === 'object' && packageEntryTab[packageName]) {
const entryTab = packageEntryTab[packageName];
const normalizedEntry = _normalizePath(entryTab);
if (tabUrls.includes(normalizedEntry)) return normalizedEntry;
}
// 3) 回退:同分包内的第一个 tab(基于分包名匹配)
return tabUrls.find(url => _getPackageName(url) === packageName) || null;
}
/** /**
* 检查tabBar是否激活 * 检查tabBar是否激活
* @param {string} linkUrl 链接路径 /pages/goods/category * @param {string} linkUrl 链接路径 /pages/goods/category
@@ -314,78 +350,37 @@ export const adaptSubpackageUrl = (url) => {
* @returns {boolean} 是否激活 * @returns {boolean} 是否激活
*/ */
export const checkTabBarActive = (linkUrl, currentPageRoute, tabBarList, packageEntryTab) => { export const checkTabBarActive = (linkUrl, currentPageRoute, tabBarList, packageEntryTab) => {
const adaptedLinkUrl = adaptSubpackageUrl(linkUrl); // 1) 归一化 linkUrl:统一前导 '/' + 子包路径适配 + 去除 query
const adaptedCurrentRoute = adaptSubpackageUrl(currentPageRoute); const normalizedLink = _normalizePath(linkUrl);
const normalizedCurrent = _normalizePath(currentPageRoute);
// 去除查询参数,仅比较路径部分 if (!normalizedLink) return false;
const cleanLinkUrl = adaptedLinkUrl.split('?')[0];
const cleanCurrentRoute = adaptedCurrentRoute.split('?')[0];
const linkUrlParts = cleanLinkUrl.split('/'); // 2) 精确匹配:当前页面就是 linkUrl 指向的页面
const currentPageRouteParts = cleanCurrentRoute.split('/'); if (normalizedLink === normalizedCurrent) return true;
// 首先尝试精确匹配(整个路径匹配) // 3) 若传入 tabBarList,使用动态判断逻辑
if (cleanLinkUrl === cleanCurrentRoute) {
return true;
}
// 如果没有精确匹配,且传入了 tabBarList,使用动态判断逻辑
if (tabBarList && tabBarList.list && Array.isArray(tabBarList.list)) { if (tabBarList && tabBarList.list && Array.isArray(tabBarList.list)) {
// 获取所有 tab 项的链接(去除查询参数)
const tabUrls = tabBarList.list const tabUrls = tabBarList.list
.filter(item => item.link && item.link.wap_url) .filter(item => item.link && item.link.wap_url)
.map(item => adaptSubpackageUrl(item.link.wap_url).split('?')[0]); .map(item => _normalizePath(item.link.wap_url));
// 检查当前 linkUrl 是否是合法的 tab URL // 检查当前 linkUrl 是否是合法的 tab URL
if (!tabUrls.includes(cleanLinkUrl)) { if (!tabUrls.includes(normalizedLink)) return false;
return false;
}
// 获取当前页面所属的分包名(parts[1]) // 找出当前页面所属的 tab
const currentPackageName = currentPageRouteParts[1]; const matchingTabUrl = _findMatchingTabUrl(currentPageRoute, tabBarList, packageEntryTab);
const linkPackageName = linkUrlParts[1];
// 如果 linkUrl 和当前页面不在同一个分包,直接返回 false // 当前页面若命中某个 tab,则只有该 tab 高亮
if (linkPackageName !== currentPackageName) { if (matchingTabUrl) return normalizedLink === matchingTabUrl;
return false;
}
// 获取当前页面路径的二级目录(parts[2]) // 4) 同分包回退:linkUrl 和当前页面在同一分包 → 高亮
const currentPageSecondPart = currentPageRouteParts[2]; const linkPackage = _getPackageName(normalizedLink);
const currentPackage = _getPackageName(normalizedCurrent);
// 检查当前页面路径是否匹配某个 tab 的精确路径 return linkPackage && currentPackage && linkPackage === currentPackage;
const currentMatchingTab = tabUrls.find(url => {
if (!url) return false;
const parts = url.split('/');
return parts[2] === currentPageSecondPart;
});
// 如果当前页面能精确匹配到某个 tab,那么只有该 tab 高亮
if (currentMatchingTab) {
return cleanLinkUrl === currentMatchingTab;
}
// 当前页面不是任何 tab 的精确匹配(是子页面)
// 使用 packageEntryTab 来确定应该高亮的 tab
if (packageEntryTab && typeof packageEntryTab === 'object') {
const entryTab = packageEntryTab[currentPackageName];
if (entryTab) {
const entryTabParts = entryTab.split('/');
return linkUrlParts[2] === entryTabParts[2];
}
}
// 基于二级路径匹配(fallback)
try {
if (linkUrlParts[2] === currentPageSecondPart) {
return true;
}
} catch (error) {
console.error('diy-bottom-nav verify error:', error);
}
return false;
} }
return false;
} }
export default { export default {

View File

@@ -1,45 +1,30 @@
<template> <template>
<view data-component-name="diy-bottom-nav" v-if="shouldShowTabBar"> <view data-component-name="diy-bottom-nav" v-if="shouldShowTabBar">
<view class="tab-bar" :style="{ backgroundColor: tabBarList.backgroundColor }"> <view class="tab-bar" :style="{ backgroundColor: tabBarList.backgroundColor }">
<view class="tabbar-border"></view> <view class="tabbar-border"></view>
<view class="item" v-for="(item, index) in tabBarList.list" :key="item.id" @tap.stop="redirectTo(item.link)"> <view class="item" v-for="(item, index) in tabBarList.list" :key="item.id" @tap.stop="redirectTo(item.link)">
<view class="bd"> <view class="bd">
<block v-if="item.link.wap_url == '/pages_goods/cart'"> <view class="icon" v-if="tabBarList.type == 1 || tabBarList.type == 2"
<view class="icon" v-if="tabBarList.type == 1 || tabBarList.type == 2" :animation="isCartItem(item.link) ? cartAnimation : null"
:animation="cartAnimation" id="tabbarCart"> :id="isCartItem(item.link) ? 'tabbarCart' : null">
<block v-if="verify(item.link)"> <block v-if="verify(item.link)">
<image v-if="item.selected_icon_type == 'img'" <image v-if="item.selected_icon_type == 'img'"
:src="$util.img(item.selectedIconPath)" /> :src="$util.img(item.selectedIconPath)" />
<diy-icon v-if="item.selected_icon_type == 'icon'" :icon="item.selectedIconPath" <diy-icon v-if="item.selected_icon_type == 'icon'" :icon="item.selectedIconPath"
:value="item.selected_style ? item.selected_style : null"></diy-icon> :value="item.selected_style ? item.selected_style : null"></diy-icon>
</block>
<block v-else>
<image v-if="item.icon_type == 'img'" :src="$util.img(item.iconPath)" />
<diy-icon v-if="item.icon_type == 'icon'" :icon="item.iconPath"
:value="item.style ? item.style : null"></diy-icon>
</block>
<view class="cart-count-mark font-size-activity-tag"
:class="{ max: item.link.wap_url == '/pages_goods/cart' && cartNumber > 99 }"
:style="{ background: 'var(--price-color)' }" v-if="cartNumber > 0">
{{ cartNumber > 99 ? '99+' : cartNumber }}
</view>
</view>
</block> </block>
<block v-else> <block v-else>
<view class="icon" v-if="tabBarList.type == 1 || tabBarList.type == 2"> <image v-if="item.icon_type == 'img'" :src="$util.img(item.iconPath)" />
<block v-if="verify(item.link)"> <diy-icon v-if="item.icon_type == 'icon'" :icon="item.iconPath"
<image v-if="item.selected_icon_type == 'img'" :value="item.style ? item.style : null"></diy-icon>
:src="$util.img(item.selectedIconPath)" />
<diy-icon v-if="item.selected_icon_type == 'icon'" :icon="item.selectedIconPath"
:value="item.selected_style ? item.selected_style : null"></diy-icon>
</block>
<block v-else>
<image v-if="item.icon_type == 'img'" :src="$util.img(item.iconPath)" />
<diy-icon v-if="item.icon_type == 'icon'" :icon="item.iconPath"
:value="item.style ? item.style : null"></diy-icon>
</block>
</view>
</block> </block>
<view class="cart-count-mark font-size-activity-tag"
:class="{ max: cartNumber > 99 }"
:style="{ background: 'var(--price-color)' }"
v-if="isCartItem(item.link) && cartNumber > 0">
{{ cartNumber > 99 ? '99+' : cartNumber }}
</view>
</view>
<view class="label" <view class="label"
v-if="(tabBarList.type == 1 || tabBarList.type == 3) && tabBarList.theme == 'diy'" v-if="(tabBarList.type == 1 || tabBarList.type == 3) && tabBarList.theme == 'diy'"
:style="{ color: verify(item.link) ? tabBarList.textHoverColor : tabBarList.textColor }"> :style="{ color: verify(item.link) ? tabBarList.textHoverColor : tabBarList.textColor }">
@@ -129,53 +114,79 @@ export default {
} }
}, },
methods: { methods: {
// 更新当前路由 // 统一归一化路径:currentPage.route 不带前导 '/',但 tab 的 wap_url 带前导 '/'
// 统一加前导 '/' 后再 split,确保 parts[1]=分包名、parts[2]=页面名
_normalizeRoute(route) {
if (!route) return '';
return route.startsWith('/') ? route : '/' + route;
},
// 从归一化路径中提取分包名(parts[1])
_getPackageName(normalizedRoute) {
const parts = normalizedRoute.split('?')[0].split('/');
return parts[1] || '';
},
// 判断某个链接是否指向购物车页面
isCartItem(link) {
if (!link || !link.wap_url) return false;
return adaptSubpackageUrl(link.wap_url).split('?')[0] === '/pages_goods/cart';
},
// 更新当前路由,追踪每个分包的入口 tab
updateCurrentRoute() { updateCurrentRoute() {
let currentPages = getCurrentPages(); const currentPages = getCurrentPages();
let currentPage = currentPages[currentPages.length - 1]; const currentPage = currentPages[currentPages.length - 1];
if (currentPage && currentPage.route) { if (!currentPage || !currentPage.route) return;
const newRoute = currentPage.route;
const newRouteParts = newRoute.split('/');
if (this.tabBarList && this.tabBarList.list) { // 归一化:补全前导 '/',确保后续 split('/') 后 parts[1]=分包名
const tabUrls = this.tabBarList.list const newRoute = this._normalizeRoute(currentPage.route);
.filter(item => item.link && item.link.wap_url) const newRouteParts = newRoute.split('?')[0].split('/');
.map(item => adaptSubpackageUrl(item.link.wap_url).split('?')[0]); const currentPackageName = newRouteParts[1] || '';
let currentMatchingTab = null; if (this.tabBarList && this.tabBarList.list && currentPackageName) {
for (const tabUrl of tabUrls) { const tabUrls = this.tabBarList.list
.filter(item => item.link && item.link.wap_url)
.map(item => adaptSubpackageUrl(item.link.wap_url).split('?')[0]);
// 1) 当前页面精确命中某个 tab → 该 tab 就是入口
let entryTab = tabUrls.find(tabUrl => tabUrl === newRoute.split('?')[0]);
// 2) 当前页面在同一分包内 → 用分包名匹配
if (!entryTab) {
entryTab = tabUrls.find(tabUrl => {
const tabParts = tabUrl.split('/'); const tabParts = tabUrl.split('/');
if (tabParts[2] === newRouteParts[2] && tabParts[1] === newRouteParts[1]) { return tabParts[1] === currentPackageName;
currentMatchingTab = tabUrl; });
break; }
}
}
let entryTab = null; // 3) 回退到上一页的分包匹配
if (currentMatchingTab) { if (!entryTab) {
entryTab = currentMatchingTab; const prevPage = currentPages[currentPages.length - 2];
} else { if (prevPage && prevPage.route) {
const prevPage = currentPages[currentPages.length - 2]; const prevParts = this._normalizeRoute(prevPage.route).split('?')[0].split('/');
if (prevPage && prevPage.route) { const prevPackageName = prevParts[1] || '';
const prevRouteParts = prevPage.route.split('/'); entryTab = tabUrls.find(tabUrl => {
for (const tabUrl of tabUrls) { const tabParts = tabUrl.split('/');
const tabParts = tabUrl.split('/'); return tabParts[1] === prevPackageName;
if (tabParts[2] === prevRouteParts[2] && tabParts[1] === prevRouteParts[1]) { });
entryTab = tabUrl; if (entryTab && prevPackageName !== currentPackageName) {
break; // 上一页在别的分包 → 只记录到上一页的分包,不污染当前分包
} this.$store.commit('setPackageEntryTab', {
} ...this.packageEntryTab,
[prevPackageName]: entryTab
});
entryTab = null;
} }
} }
if (entryTab) {
const updatedEntryTab = { ...this.packageEntryTab, [newRouteParts[1]]: entryTab };
this.$store.commit('setPackageEntryTab', updatedEntryTab);
}
} }
this.currentRoute = newRoute; if (entryTab) {
this.$store.commit('setPackageEntryTab', {
...this.packageEntryTab,
[currentPackageName]: entryTab
});
}
} }
this.currentRoute = newRoute;
}, },
redirectTo(link) { redirectTo(link) {
this.$emit('callback'); this.$emit('callback');
@@ -183,11 +194,13 @@ export default {
if (link && link.wap_url) { if (link && link.wap_url) {
const decodedUrl = decodeEntity(link.wap_url); const decodedUrl = decodeEntity(link.wap_url);
const adaptedUrl = adaptSubpackageUrl(decodedUrl); const adaptedUrl = adaptSubpackageUrl(decodedUrl);
const urlParts = adaptedUrl.split('/'); const packageName = this._getPackageName(adaptedUrl);
if (urlParts.length >= 2 && urlParts[1]) { if (packageName) {
const updatedEntryTab = { ...this.packageEntryTab, [urlParts[1]]: adaptedUrl }; this.$store.commit('setPackageEntryTab', {
this.$store.commit('setPackageEntryTab', updatedEntryTab); ...this.packageEntryTab,
[packageName]: adaptedUrl.split('?')[0]
});
} }
} }
@@ -196,17 +209,17 @@ export default {
verify(link) { verify(link) {
if (link == null || link == '' || !link.wap_url) return false; if (link == null || link == '' || !link.wap_url) return false;
let currentPages = getCurrentPages(); const currentPages = getCurrentPages();
let currentPage = currentPages[currentPages.length - 1]; const currentPage = currentPages[currentPages.length - 1];
let currentPageRoute = currentPage && currentPage.route ? '/' + currentPage.route : ''; const currentPageRoute = currentPage && currentPage.route
let linkUrl = link.wap_url; ? this._normalizeRoute(currentPage.route)
: '';
if (linkUrl === this.$util.INDEX_PAGE_URL && this.name === 'DIY_VIEW_INDEX') { if (link.wap_url === this.$util.INDEX_PAGE_URL && this.name === 'DIY_VIEW_INDEX') {
return true; return true;
} }
// 使用通用的 checkTabBarActive 函数来判断 tab 高亮状态 return checkTabBarActive(link.wap_url, currentPageRoute, this.tabBarList, this.packageEntryTab);
return checkTabBarActive(linkUrl, currentPageRoute, this.tabBarList, this.packageEntryTab);
} }
} }
}; };

View File

@@ -14,7 +14,7 @@ const store = new Vuex.Store({
token: null, token: null,
siteInfo: null, siteInfo: null,
memberInfo: null, memberInfo: null,
tabBarList: '', tabBarList: null,
siteState: 1, siteState: 1,
themeStyle: '', themeStyle: '',
themeColor: '', themeColor: '',