Files
lucky_shop/components-diy/diy-bottom-nav.vue
ZF sun 2d8846c64c feat: 新增多语言文案、优化导航及工具函数,修复部分业务逻辑
1. 新增多语言资源文件,覆盖文章、预约日期、会员信息、优惠券、表单、购物车、积分商城、分销、AI聊天等多个模块
2. 优化底部导航组件,使用多语言配置替代硬编码文案
3. 重构工具函数,新增从后端数据提取多语言文案的工具方法
4. 修复邀请好友页面的保存提示文案,替换为多语言版本
5. 修复商品SKU组件的提示文案,使用多语言配置
6. 重构AI聊天页面的帮助文档和回复模板,使用多语言配置
7. 优化空状态组件,支持多语言默认文案
8. 修复跳转逻辑,补全参数传递
9. 新增本地开发环境配置
2026-06-25 18:15:31 +08:00

390 lines
11 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');
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]
});
}
}
this.$util.diyRedirectTo(link);
},
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)
: '';
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);
}
}
};
</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;
position: fixed;
left: 0;
bottom: 0;
width: 100%;
z-index: 998;
display: flex;
border-top: 2rpx solid #f5f5f5;
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>