Files
lucky_shop/components-diy/diy-bottom-nav.vue
ZF sun 313c283d9b fix: 适配华为快应用WebView场景并修复tabbar定位问题
1. 新增华为快应用WebView环境检测工具函数
2. 文章组件在快应用WebView中强制关闭滚动模式
3. 修复全局tabbar在H5/快应用中的定位漂移问题,加固fixed定位样式
4. 补全页面配置中的导航栏标题字段
2026-07-15 15:00:42 +08:00

450 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view data-component-name="diy-bottom-nav" v-if="shouldShowTabBar">
<view class="tab-bar" :style="{ backgroundColor: tabBarList.backgroundColor }">
<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="bd">
<view class="icon" v-if="tabBarList.type == 1 || tabBarList.type == 2"
:animation="isCartItem(item.link) ? cartAnimation : null"
:id="isCartItem(item.link) ? 'tabbarCart' : null">
<block v-if="verify(item.link)">
<image v-if="item.selected_icon_type == 'img'"
: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 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"
v-if="(tabBarList.type == 1 || tabBarList.type == 3) && tabBarList.theme == 'diy'"
:style="{ color: verify(item.link) ? tabBarList.textHoverColor : tabBarList.textColor }">
{{ $langConfig.getField(item, 'text') }}
</view>
<view class="label"
v-if="(tabBarList.type == 1 || tabBarList.type == 3) && tabBarList.theme == 'default'"
:style="{ color: verify(item.link) ? 'var(--base-color)' : '#333333' }">
{{ $langConfig.getField(item, 'text') }}
</view>
</view>
</view>
</view>
<!-- 解决fixed定位后底部导航栏塌陷问题 -->
<view class="tab-bar-placeholder"></view>
</view>
</template>
<script>
import { adaptSubpackageUrl, checkTabBarActive, decodeEntity } from '@/common/js/util.js'
import DiyMinx from './minx.js'
// 底部导航栏
export default {
name: 'diy-bottom-nav',
props: {
value: {
type: Object
},
name: {
type: String,
default: ''
}
},
mixins: [DiyMinx],
data() {
return {
currentRoute: '', //当前页面路径
jumpFlag: true, //是否可以跳转,防止重复点击
cartAnimation: {}
};
},
mounted() {
this.updateCurrentRoute();
this.$nextTick(() => {
if (!this.$store.state.cartPosition) {
let query = uni.createSelectorQuery().in(this);
query.select('#tabbarCart')
.boundingClientRect(data => {
if (data) this.$store.commit('setCartPosition', data);
}).exec();
query.select('.tab-bar')
.boundingClientRect(data => {
if (data) this.$store.commit('setTabBarHeight', data.height +
'px');
}).exec();
}
});
// 监听页面显示事件,更新当前路由
this.$on('hook:onShow', () => {
this.updateCurrentRoute();
});
},
computed: {
// 当前语言(响应式):通过 componentRefresh 建立依赖,切语言时自动更新
// 不能直接用 uni.getStorageSync 作为 data,因为它不是响应式的
currentLang() {
// 通过 componentRefresh 建立响应式依赖
// 切语言时 store 中的 componentRefresh 会 +1,触发重新计算
this.componentRefresh;
return uni.getStorageSync("lang") || 'zh-cn';
},
cartChange() {
return this.$store.state.cartChange;
},
packageEntryTab() {
return this.$store.state.packageEntryTab;
},
},
watch: {
cartChange: function (nval, oval) {
if (nval > oval) {
let animation = uni.createAnimation({
duration: 200,
timingFunction: 'ease'
});
animation.scale(1.2).step();
this.cartAnimation = animation.export();
setTimeout(() => {
animation.scale(1).step();
this.cartAnimation = animation.export();
}, 300);
}
}
},
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() {
const currentPages = getCurrentPages();
const currentPage = currentPages[currentPages.length - 1];
if (!currentPage || !currentPage.route) return;
// 归一化:补全前导 '/',确保后续 split('/') 后 parts[1]=分包名
const newRoute = this._normalizeRoute(currentPage.route);
const newRouteParts = newRoute.split('?')[0].split('/');
const currentPackageName = newRouteParts[1] || '';
if (this.tabBarList && this.tabBarList.list && currentPackageName) {
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('/');
return tabParts[1] === currentPackageName;
});
}
// 3) 回退到上一页的分包匹配
if (!entryTab) {
const prevPage = currentPages[currentPages.length - 2];
if (prevPage && prevPage.route) {
const prevParts = this._normalizeRoute(prevPage.route).split('?')[0].split('/');
const prevPackageName = prevParts[1] || '';
entryTab = tabUrls.find(tabUrl => {
const tabParts = tabUrl.split('/');
return tabParts[1] === prevPackageName;
});
if (entryTab && prevPackageName !== currentPackageName) {
// 上一页在别的分包 → 只记录到上一页的分包,不污染当前分包
this.$store.commit('setPackageEntryTab', {
...this.packageEntryTab,
[prevPackageName]: entryTab
});
entryTab = null;
}
}
}
if (entryTab) {
this.$store.commit('setPackageEntryTab', {
...this.packageEntryTab,
[currentPackageName]: entryTab
});
}
}
this.currentRoute = newRoute;
},
redirectTo(link) {
this.$emit('callback');
const redirectLink = link ? { ...link } : link;
if (link && link.wap_url) {
const decodedUrl = decodeEntity(link.wap_url);
const adaptedUrl = adaptSubpackageUrl(decodedUrl);
const packageName = this._getPackageName(adaptedUrl);
if (packageName) {
this.$store.commit('setPackageEntryTab', {
...this.packageEntryTab,
[packageName]: adaptedUrl.split('?')[0]
});
}
// 若目标页面不是 TabBar 根页面,追加 from_tab 来源参数,
// 用于非 Tab 页面的 TabBar 高亮记忆
if (this.tabBarList && this.tabBarList.list) {
const tabUrls = this.tabBarList.list
.filter(item => item.link && item.link.wap_url)
.map(item => adaptSubpackageUrl(decodeEntity(item.link.wap_url)).split('?')[0]);
if (!tabUrls.includes(adaptedUrl.split('?')[0])) {
const currentPages = getCurrentPages();
const currentPage = currentPages[currentPages.length - 1];
const currentTab = currentPage && currentPage.route
? tabUrls.find(tabUrl => checkTabBarActive(tabUrl, '/' + currentPage.route, this.tabBarList, this.packageEntryTab))
: null;
if (currentTab) {
redirectLink.from_tab = currentTab;
}
}
}
}
this.$util.diyRedirectTo(redirectLink);
},
verify(link) {
if (link == null || link == '' || !link.wap_url) return false;
const currentPages = getCurrentPages();
const currentPage = currentPages[currentPages.length - 1];
const currentPageRoute = currentPage && currentPage.route
? this._normalizeRoute(currentPage.route)
: '';
// 读取当前页面 query 参数(兼容小程序与 H5)
let currentPageQuery = {};
if (currentPage) {
if (currentPage.options) {
currentPageQuery = currentPage.options;
} else if (currentPage.$page && currentPage.$page.options) {
currentPageQuery = currentPage.$page.options;
} else if (currentPage.$route && currentPage.$route.query) {
currentPageQuery = currentPage.$route.query;
}
}
if (link.wap_url === this.$util.INDEX_PAGE_URL && this.name === 'DIY_VIEW_INDEX') {
return true;
}
return checkTabBarActive(link.wap_url, currentPageRoute, this.tabBarList, this.packageEntryTab, currentPageQuery);
}
}
};
</script>
<style lang="scss">
.placeholder {
height: 112rpx;
&.bluge {
height: 180rpx;
}
}
.safe-area {
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.tab-bar {
background-color: #fff;
box-sizing: border-box;
/* ============================================================
* 固定在最底端,不能位置漂移(微信小程序 / 鸿蒙元服务 / 快应用 H5)
* ------------------------------------------------------------
* 现象(只在客户端小程序里复现,浏览器 H5 没事):
* 页面带 navigationStyle:custom + 页面内有 scroll-view,
* 滚到底部时 .tab-bar 会被微信客户端的页面避让逻辑推上去,
* 出现"位置漂移"。
*
* 根因(微信小程序特性):
* 1) 微信小程序里 position:fixed 的 containing block 是 page
* 根容器(不是 viewport),可视区变化时 fixed 元素下沿会
* 跟着重算,造成漂移。
* 2) 父级一旦出现 transform/filter/will-change,fixed 的
* containing block 会被进一步改写,fixed 元素行为更不可控。
*
* 加固措施:
* - bottom: 0 !important 钉死下沿
* - 提升 z-index 到 2147483647,避免被其他 fixed 元素遮挡
* - transform: translateZ(0) 强制提升到独立合成层,降低父级
* transform 干扰;will-change:transform 提示浏览器持续合成
* - 微信小程序里 #ifdef MP-WEIXIN / #ifdef MP-HARMONY 走原生
* tabBar,diy-bottom-nav 通常不会同时存在,但保留兜底
* ============================================================ */
position: fixed !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
top: auto !important;
width: 100%;
z-index: 2147483647 !important;
display: flex;
border-top: 2rpx solid #f5f5f5;
/* 强制提升合成层,降低父级 transform/filter 改变 containing block 的概率 */
transform: translateZ(0) !important;
-webkit-transform: translateZ(0) !important;
will-change: transform !important;
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
.tabbar-border {
background-color: rgba(255, 255, 255, 0.329412);
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 2rpx;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
}
.item {
display: flex;
align-items: center;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
flex: 1;
flex-direction: column;
padding-bottom: 10rpx;
box-sizing: border-box;
.bd {
position: relative;
height: 100rpx;
flex-direction: column;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
.icon {
position: relative;
display: inline-block;
margin-top: 10rpx;
width: 40rpx;
height: 40rpx;
font-size: 40rpx;
line-height: 40rpx;
image {
width: 100%;
height: 100%;
}
>view {
height: inherit;
display: flex;
align-items: center;
}
.bar-icon {
font-size: 42rpx;
}
}
.label {
position: relative;
text-align: center;
font-size: 24rpx;
line-height: 1;
margin-top: 12rpx;
}
}
&.bulge {
.bd {
position: relative;
height: 100rpx;
flex-direction: column;
text-align: center;
.icon {
margin-top: -60rpx;
margin-bottom: 4rpx;
border-radius: 50%;
width: 100rpx;
height: 102rpx;
padding: 10rpx;
border-top: 2rpx solid #f5f5f5;
background-color: #fff;
box-sizing: border-box;
image {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.label {
position: relative;
text-align: center;
font-size: 24rpx;
height: 40rpx;
line-height: 40rpx;
}
}
}
.cart-count-mark {
position: absolute;
top: -8rpx;
right: -18rpx;
width: 24rpx;
height: 24rpx !important;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
padding: 6rpx;
border-radius: 50%;
z-index: 99;
&.max {
width: 40rpx;
border-radius: 24rpx;
right: -28rpx;
}
}
}
}
.tab-bar-placeholder {
padding-bottom: calc(constant(safe-area-inset-bottom) + 112rpx);
padding-bottom: calc(env(safe-area-inset-bottom) + 112rpx);
}
</style>