hookehuyr

feat: 添加小程序跳转支持并重构底部导航栏

- 新增 miniProgram.js 工具模块,提供 H5 环境下检测微信小程序 webview 及页面跳转能力
- 重构 AppTabbar 组件,替换 NutUI Tabbar 为自定义实现,优化视觉样式
- 在 H5 环境下优先尝试使用小程序桥接跳转页面,增强多端体验一致性
<template>
<view class="app-tabbar">
<nut-tabbar
:model-value="activeTab"
bottom
placeholder
safe-area-inset-bottom
:active-color="activeColor"
:unactive-color="inactiveColor"
@tab-switch="handleTabSwitch"
>
<nut-tabbar-item
<view class="app-tabbar__placeholder" />
<view class="app-tabbar__wrap">
<view class="app-tabbar__panel">
<view
v-for="item in tabItems"
:key="item.name"
:name="item.name"
:tab-title="item.title"
class="app-tabbar__item"
:class="{ 'is-active': isActive(item.name) }"
@tap="handleTabClick(item)"
>
<template #icon="{ active }">
<view class="app-tabbar__item-inner">
<view class="app-tabbar__icon">
<component
:is="item.icon"
size="18"
:color="active ? activeColor : inactiveColor"
:color="isActive(item.name) ? activeColor : inactiveColor"
/>
</template>
</nut-tabbar-item>
</nut-tabbar>
</view>
<text class="app-tabbar__label">{{ item.title }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
import Taro from '@tarojs/taro'
import { Home, Message, My } from '@nutui/icons-vue-taro'
// 迁移到独立 H5 项目时,别忘了把 miniProgram helper 一起带走。
import { isH5Env, navigateToMiniProgramPage } from '@/utils/miniProgram'
const props = defineProps({
current: {
......@@ -63,44 +64,88 @@ const tabItems = [
},
]
const activeTab = computed(() => props.current)
const isActive = (name) => name === props.current
const handleTabSwitch = (...args) => {
const nextTab = args[1]
const target = tabItems.find((item) => item.name === nextTab)
const handleTabClick = async (item) => {
if (!item?.url || item.name === props.current) {
return
}
if (!target || target.name === props.current) {
if (isH5Env()) {
try {
const has_navigated = await navigateToMiniProgramPage(item.url)
if (has_navigated) {
return
}
} catch (error) {
console.error('H5 跳转小程序页面失败:', error)
}
}
Taro.redirectTo({
url: target.url,
})
Taro.redirectTo({ url: item.url })
}
</script>
<style lang="less">
.app-tabbar {
:deep(.nut-tabbar) {
left: 24rpx;
right: 24rpx;
bottom: 24rpx;
width: auto;
border: 2rpx solid rgba(166, 121, 57, 0.12);
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 24rpx 60rpx rgba(15, 23, 42, 0.12);
overflow: hidden;
.app-tabbar__placeholder {
height: 132rpx;
flex-shrink: 0;
}
.app-tabbar__wrap {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
box-sizing: border-box;
}
:deep(.nut-tabbar-item_icon-box) {
padding-top: 8rpx;
.app-tabbar__panel {
display: flex;
align-items: stretch;
min-height: 132rpx;
padding: 16rpx 24rpx;
border-top: 2rpx solid rgba(166, 121, 57, 0.12);
background: rgba(255, 255, 255, 0.98);
box-sizing: border-box;
backdrop-filter: blur(12rpx);
}
:deep(.nut-tabbar-item_icon-box_nav-word) {
margin-top: 8rpx;
.app-tabbar__item {
flex: 1;
min-width: 0;
}
.app-tabbar__item-inner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10rpx;
min-height: 100rpx;
border-radius: 20rpx;
color: #8b95a7;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.app-tabbar__icon {
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.app-tabbar__label {
font-size: 22rpx;
font-weight: 600;
line-height: 1.2;
}
.app-tabbar__item.is-active .app-tabbar__item-inner {
color: #a67939;
}
}
</style>
......
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js'
let wechat_bridge_loading_promise = null
const isBrowserEnv = () => typeof window !== 'undefined' && typeof document !== 'undefined'
const getWechatBridge = () => {
if (typeof window === 'undefined') {
return null
}
return window.wx || window.jWeixin || null
}
const getMiniProgramBridge = () => getWechatBridge()?.miniProgram || null
export const isH5Env = () => isBrowserEnv()
const ensureWechatBridge = async () => {
if (!isH5Env()) {
return null
}
const current_bridge = getWechatBridge()
if (current_bridge) {
return current_bridge
}
if (!wechat_bridge_loading_promise) {
wechat_bridge_loading_promise = new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = WECHAT_JS_SDK_URL
script.async = true
script.onload = () => resolve(getWechatBridge())
script.onerror = () => reject(new Error('微信 JS SDK 加载失败'))
document.head.appendChild(script)
})
}
try {
return await wechat_bridge_loading_promise
} catch (error) {
wechat_bridge_loading_promise = null
throw error
}
}
export const isWechatMiniProgramWebview = () => new Promise((resolve) => {
const check_env = async () => {
if (!isH5Env()) {
resolve(false)
return
}
try {
await ensureWechatBridge()
} catch (error) {
console.error('微信 JS SDK 初始化失败:', error)
resolve(false)
return
}
const mini_program_bridge = getMiniProgramBridge()
if (!mini_program_bridge) {
resolve(false)
return
}
if (typeof mini_program_bridge.getEnv !== 'function') {
resolve(true)
return
}
mini_program_bridge.getEnv((result = {}) => {
resolve(!!result.miniprogram)
})
}
check_env().catch(() => {
resolve(false)
})
})
export const navigateToMiniProgramPage = async (url = '') => {
const normalized_url = String(url || '').trim()
if (!normalized_url) {
return false
}
const is_supported_webview = await isWechatMiniProgramWebview()
if (!is_supported_webview) {
return false
}
const mini_program_bridge = getMiniProgramBridge()
if (typeof mini_program_bridge?.navigateTo !== 'function') {
return false
}
return new Promise((resolve, reject) => {
mini_program_bridge.navigateTo({
url: normalized_url,
success: () => resolve(true),
fail: (error) => reject(error),
})
})
}