319 行
7.5 KiB
Vue
319 行
7.5 KiB
Vue
<template>
|
||
<view class="fc-tabbar">
|
||
<view class="fc-tabbar__nav">
|
||
<view
|
||
v-for="(item, idx) in tabs"
|
||
:key="item.path"
|
||
class="fc-tabbar__item"
|
||
:class="{
|
||
'fc-tabbar__item--active': current === idx,
|
||
'fc-tabbar__item--primary': item.primary
|
||
}"
|
||
hover-class="none"
|
||
@click="switchTo(item, idx)"
|
||
>
|
||
<!-- 中间突出加号悬浮球(发布) -->
|
||
<template v-if="item.primary">
|
||
<view class="fc-tabbar__circle-wrap">
|
||
<view class="fc-tabbar__circle-btn">
|
||
<image class="fc-tabbar__plus-icon" :src="iconUrl('plus-white')" mode="aspectFit" />
|
||
</view>
|
||
</view>
|
||
<text class="fc-tabbar__text fc-tabbar__text--primary">{{ item.text }}</text>
|
||
</template>
|
||
|
||
<!-- 普通常驻导航项 -->
|
||
<template v-else>
|
||
<view class="fc-tabbar__icon">
|
||
<image
|
||
class="fc-tabbar__img"
|
||
:src="iconUrl(current === idx ? item.active : item.icon)"
|
||
mode="aspectFit"
|
||
/>
|
||
<!-- 未读通知红点/数字徽章 -->
|
||
<view
|
||
v-if="item.badgeKey === 'message' && unreadCount > 0"
|
||
class="fc-tabbar__badge"
|
||
>
|
||
<text class="fc-tabbar__badge-text">{{ badgeText }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="fc-tabbar__text">{{ item.text }}</text>
|
||
</template>
|
||
</view>
|
||
</view>
|
||
<view class="fc-safe-bottom"></view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { iconUrl } from '@/common/store/ui.js'
|
||
import messageApi from '@/common/api/message.js'
|
||
import { useUserStore } from '@/common/store/index.js'
|
||
|
||
export default {
|
||
name: 'FcTabbar',
|
||
options: {
|
||
/** 开启虚拟化宿主节点,避免小程序自定义组件样式隔离产生非预期布局 */
|
||
virtualHost: true,
|
||
},
|
||
props: {
|
||
/** 当前激活的 Tab 索引(0:首页 1:分类 2:发布 3:通知 4:我的) */
|
||
current: { type: Number, default: 0 },
|
||
},
|
||
data() {
|
||
return {
|
||
unreadCount: 0,
|
||
tabs: [
|
||
{ text: '首页', icon: 'tab-home', active: 'tab-home-on', path: '/pages/index/index' },
|
||
{ text: '分类', icon: 'tab-category', active: 'tab-category-on', path: '/pages/category/index' },
|
||
{ text: '发布', icon: 'tab-post', active: 'tab-post-on', path: '/pages/post/create', primary: true },
|
||
{ text: '通知', icon: 'tab-message', active: 'tab-message-on', path: '/pages/message/index', badgeKey: 'message' },
|
||
{ text: '我的', icon: 'tab-mine', active: 'tab-mine-on', path: '/pages/mine/index' },
|
||
],
|
||
}
|
||
},
|
||
computed: {
|
||
/**
|
||
* 格式化未读角标文本(超过99显示99+)
|
||
* @returns {string} 角标数字文本
|
||
*/
|
||
badgeText() {
|
||
if (this.unreadCount > 99) return '99+'
|
||
return String(this.unreadCount)
|
||
},
|
||
},
|
||
mounted() {
|
||
this.fetchUnread()
|
||
uni.$on('refreshUnreadCount', this.fetchUnread)
|
||
},
|
||
beforeDestroy() {
|
||
uni.$off('refreshUnreadCount', this.fetchUnread)
|
||
},
|
||
methods: {
|
||
/**
|
||
* 暴露给模板:获取对应图标的远程或本地地址
|
||
* @param {string} key 图标标识键
|
||
* @returns {string} 完整的图标 URL
|
||
*/
|
||
iconUrl,
|
||
/**
|
||
* 静默拉取当前用户未读消息总数
|
||
*/
|
||
async fetchUnread() {
|
||
let logged = false
|
||
try {
|
||
logged = !!useUserStore().isLogin.value
|
||
} catch (e) {}
|
||
if (!logged) {
|
||
this.unreadCount = 0
|
||
return
|
||
}
|
||
try {
|
||
const res = await messageApi.unreadCount()
|
||
this.unreadCount = Number((res && res.count) || 0)
|
||
} catch (e) {
|
||
this.unreadCount = 0
|
||
}
|
||
},
|
||
/**
|
||
* 切换底部导航目标页面
|
||
* @param {Object} item 目标 Tab 配置对象
|
||
* @param {number} idx 目标 Tab 索引
|
||
*/
|
||
switchTo(item, idx) {
|
||
if (idx === this.current) return
|
||
|
||
// 中间突出加号(发布):作为独立模态表单页推入,保留前序页面栈,支持原路返回
|
||
if (item.primary) {
|
||
uni.navigateTo({ url: item.path })
|
||
return
|
||
}
|
||
|
||
// 其余常驻 Tab 页面平滑切换:H5 环境优先 redirectTo 避免 reLaunch 闪屏
|
||
// #ifdef H5
|
||
uni.redirectTo({
|
||
url: item.path,
|
||
fail: () => {
|
||
uni.reLaunch({ url: item.path })
|
||
},
|
||
})
|
||
// #endif
|
||
// #ifndef H5
|
||
uni.reLaunch({ url: item.path })
|
||
// #endif
|
||
},
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.fc-tabbar {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 999;
|
||
background: rgba(255, 255, 255, 0.98);
|
||
-webkit-backdrop-filter: blur(20px);
|
||
backdrop-filter: blur(20px);
|
||
border-top: 1rpx solid rgba(0, 210, 106, 0.08);
|
||
box-shadow: 0 -4rpx 24rpx rgba(0, 50, 25, 0.04);
|
||
box-sizing: border-box;
|
||
-webkit-tap-highlight-color: transparent !important;
|
||
user-select: none;
|
||
}
|
||
|
||
.fc-tabbar__nav {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-around;
|
||
height: 104rpx;
|
||
padding-top: 4rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.fc-tabbar__item {
|
||
flex: 1;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
position: relative;
|
||
background: transparent !important;
|
||
outline: none !important;
|
||
-webkit-tap-highlight-color: transparent !important;
|
||
|
||
// 普通导航项点击时仅对图标进行柔和的弹性下压微动效,防止整格形变引起方框闪烁
|
||
&:active {
|
||
.fc-tabbar__img {
|
||
transform: scale(0.88);
|
||
}
|
||
}
|
||
|
||
&--active {
|
||
.fc-tabbar__text {
|
||
color: #00A854;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
// 发布按钮项:点击时仅触发内部圆形球的物理下压弹性动效与光影收敛
|
||
&--primary {
|
||
&:active {
|
||
.fc-tabbar__circle-btn {
|
||
transform: scale(0.91) translateY(4rpx);
|
||
box-shadow: 0 4rpx 14rpx rgba(0, 210, 106, 0.42);
|
||
}
|
||
.fc-tabbar__text--primary {
|
||
opacity: 0.85;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 普通导航图标区 */
|
||
.fc-tabbar__icon {
|
||
height: 48rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
}
|
||
|
||
.fc-tabbar__img {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
transition: transform 0.16s cubic-bezier(0.2, 0, 0.2, 1);
|
||
}
|
||
|
||
/* 未读消息数字/红点徽章(Badge) */
|
||
.fc-tabbar__badge {
|
||
position: absolute;
|
||
top: -6rpx;
|
||
right: -16rpx;
|
||
z-index: 10;
|
||
min-width: 28rpx;
|
||
height: 28rpx;
|
||
padding: 0 8rpx;
|
||
border-radius: 14rpx;
|
||
background: linear-gradient(135deg, #FF5B5B 0%, #FF3333 100%);
|
||
border: 2rpx solid #ffffff;
|
||
box-shadow: 0 2rpx 8rpx rgba(255, 51, 51, 0.35);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
animation: badgePop 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
}
|
||
|
||
.fc-tabbar__badge-text {
|
||
font-size: 18rpx;
|
||
color: #ffffff;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
transform: scale(0.92);
|
||
}
|
||
|
||
@keyframes badgePop {
|
||
0% {
|
||
transform: scale(0.4);
|
||
opacity: 0;
|
||
}
|
||
100% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* 导航文字 */
|
||
.fc-tabbar__text {
|
||
font-size: 20rpx;
|
||
color: #64748B;
|
||
margin-top: 4rpx;
|
||
line-height: 1.2;
|
||
transition: color 0.16s ease;
|
||
|
||
&--primary {
|
||
color: #00A854;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
/* 中间突出圆形发布按钮(FAB 浮动发布球) */
|
||
.fc-tabbar__circle-wrap {
|
||
position: relative;
|
||
width: 96rpx;
|
||
height: 48rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.fc-tabbar__circle-btn {
|
||
position: absolute;
|
||
top: -44rpx;
|
||
width: 90rpx;
|
||
height: 90rpx;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #00E070 0%, #00D26A 100%);
|
||
border: 6rpx solid #ffffff;
|
||
box-shadow: 0 10rpx 26rpx rgba(0, 210, 106, 0.35), 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: transform 0.16s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.16s ease;
|
||
box-sizing: border-box;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.fc-tabbar__plus-icon {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 60, 30, 0.18));
|
||
}
|
||
</style>
|