文件
fenleixinxi/uniapp/components/fc-tabbar/fc-tabbar.vue
MiAn 14d02aedcd feat: 新增提审防审模式,隐藏发布及评论相关入口
本次新增由后台 virtualPayAudit 开关控制的审核模式,在该模式下:
1. 隐藏底部 Tab 栏的发布按钮
2. 隐藏空状态、分类页、个人页的发布入口
3. 隐藏帖子卡片、详情页、个人发布页的评论数及评价入口
4. 发布页在审核模式下直接拦截并跳转至首页
5. 详情页的用户评价及评价弹层在审核模式下隐藏,避免 UGC 判定
2026-11-04 16:26:25 +08:00

343 行
8.5 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<view class="fc-tabbar">
<view class="fc-tabbar__nav">
<view
v-for="item in displayTabs"
:key="item.path"
class="fc-tabbar__item"
:class="{
'fc-tabbar__item--active': item.rawIndex === current,
'fc-tabbar__item--primary': item.primary
}"
hover-class="none"
@click="switchTo(item)"
>
<!-- 中间突出加号悬浮球(发布) -->
<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(item.rawIndex === current ? 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, uiState } from '@/common/store/ui.js'
import messageApi from '@/common/api/message.js'
import { useUserStore } from '@/common/store/index.js'
import { safeNavigateTo, safeRedirectTo } from '@/common/utils/nav.js'
// 未读消息请求节流时间戳(毫秒)
let lastFetchUnreadAt = 0
let cachedUnreadCount = 0
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', rawIndex: 0 },
{ text: '分类', icon: 'tab-category', active: 'tab-category-on', path: '/pages/category/index', rawIndex: 1 },
{ text: '发布', icon: 'tab-post', active: 'tab-post-on', path: '/pages/post/create', primary: true, rawIndex: 2 },
{ text: '通知', icon: 'tab-message', active: 'tab-message-on', path: '/pages/message/index', badgeKey: 'message', rawIndex: 3 },
{ text: '我的', icon: 'tab-mine', active: 'tab-mine-on', path: '/pages/mine/index', rawIndex: 4 },
],
}
},
computed: {
/**
* 是否处于提审防审模式(由后台 virtualPayAudit 开关统一控制)
* @returns {boolean} true=审核中(隐藏发布按钮); false=正常运营模式
*/
isAudit() {
return uiState.virtualPayAudit !== false
},
/**
* 实际渲染呈现的 Tab 项列表(审核模式下彻底隐藏加号发布球)
* @returns {Array<Object>} 过滤后的 Tab 列表
*/
displayTabs() {
if (this.isAudit) {
return this.tabs.filter((t) => !t.primary && t.path !== '/pages/post/create')
}
return this.tabs
},
/**
* 格式化未读角标文本(超过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,
/**
* 静默拉取当前用户未读消息总数(带 10 秒防抖节流,避免高频切页重复刷接口)
* @param {boolean} force 是否强制跳过缓存刷新
*/
async fetchUnread(force = false) {
let logged = false
try {
logged = !!useUserStore().isLogin.value
} catch (e) {}
if (!logged) {
this.unreadCount = 0
cachedUnreadCount = 0
return
}
const now = Date.now()
if (!force && now - lastFetchUnreadAt < 10000) {
this.unreadCount = cachedUnreadCount
return
}
try {
const res = await messageApi.unreadCount()
const count = Number((res && res.count) || 0)
this.unreadCount = count
cachedUnreadCount = count
lastFetchUnreadAt = now
} catch (e) {
this.unreadCount = cachedUnreadCount || 0
}
},
/**
* 切换底部导航目标页面
* 杜绝 reLaunch 全局冷重启白屏,常驻 Tab 统一采用 safeRedirectTo 平滑切换
* @param {Object} item 目标 Tab 配置对象
*/
switchTo(item) {
if (item.rawIndex === this.current) return
// 中间突出加号(发布):作为独立模态表单页推入,保留前序页面栈,支持原路返回
if (item.primary) {
safeNavigateTo(item.path)
return
}
// 其余常驻 Tab 页面平滑切换:杜绝 reLaunch 造成的整个小程序销毁与白屏
safeRedirectTo(item.path)
},
},
}
</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>