hookehuyr

feat: 新增 TabBar 红点提醒功能(预开发)

- 添加功能开关配置系统(src/config/features.js)
  - 支持全局开关控制功能启用/禁用
  - 可配置红点字段名称和显示阈值
  - 方便灰度发布和功能回滚

- TabBar 组件自动从 Store 读取红点状态
  - 移除手动传递 badges prop
  - 组件内部自动管理红点显示逻辑
  - 只处理"我的"按钮的红点

- User Store 新增红点状态自动计算
  - 新增 tabBarBadges 计算属性
  - 根据用户信息自动计算红点状态
  - 支持数字和布尔类型字段
  - 响应式更新,无需手动管理

- 首页添加用户信息自动刷新
  - useShow 生命周期自动刷新
  - 只在已登录状态下请求
  - 添加错误处理

- 创建完整的使用文档(docs/features/tabbar-badge.md)
  - 功能说明和启用方法
  - 数据流程图和测试方法
  - 常见问题解答

功能默认关闭(features.tabbarBadge = false)
当前使用 unread_count 字段(待确认)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -5,6 +5,50 @@
---
## [2026-02-03] - 新增 TabBar 红点提醒功能(预开发)
### 新增
- TabBar 红点提醒功能(`src/components/TabBar.vue`
- 在"我的"按钮右上角显示红点,提示用户有未读消息
- 红点状态由用户信息接口返回的 `unread_count` 字段决定
- 支持通过配置文件全局开关功能
- 功能开关配置系统(`src/config/features.js`
- 集中管理功能的启用/禁用状态
- 支持灰度发布和功能回滚
- 可配置红点字段名称和显示阈值
- User Store 红点状态自动计算(`src/stores/user.js`
- 新增 `tabBarBadges` 计算属性,自动根据用户信息计算红点状态
- 支持数字和布尔类型字段
- 响应式更新,无需手动管理
- 首页用户信息自动刷新(`src/pages/index/index.vue`
- 页面显示时自动刷新用户信息,更新红点状态
- 只在已登录状态下请求,避免无效调用
- 添加错误处理,提升健壮性
### 优化
- TabBar 组件自动从 Store 读取红点状态
- 移除手动传递的 `badges` prop
- 组件内部自动管理红点显示逻辑
- 简化使用方式,降低维护成本
---
**详细信息**
- **影响文件**:
- `src/config/features.js` (新建)
- `src/components/TabBar.vue`
- `src/stores/user.js`
- `src/pages/index/index.vue`
- **技术栈**: Vue 3, Pinia, Composition API, Computed
- **测试状态**: ⚠️ 预开发阶段(功能开关默认关闭)
- **备注**:
- 功能默认关闭,等后端接口字段确定后再开启
- 当前使用 `unread_count` 字段,后续可能调整
- 红点显示阈值默认为 1(即有未读消息时显示)
- 完整使用文档:`docs/features/tabbar-badge.md`
---
## [2026-02-03] - 优化搜索页清空逻辑和引导文案
### 优化
......
# TabBar 红点提醒功能
## 📖 功能说明
TabBar 红点提醒功能用于在"我的"按钮右上角显示红点,提示用户有未读消息或通知。
## ⚙️ 功能开关
### 配置文件:`src/config/features.js`
```javascript
export const features = {
// 🔴 功能总开关(默认关闭)
tabbarBadge: false,
// 📊 字段名称(根据后端实际返回调整)
tabbarBadgeField: 'unread_count', // 当前使用 'unread_count'
// 🎯 显示阈值
tabbarBadgeThreshold: 1 // 当 unread_count >= 1 时显示红点
}
```
## 🚀 启用功能
### 方式 1: 修改配置文件(推荐)
```javascript
// src/config/features.js
export const features = {
tabbarBadge: true, // ✅ 改为 true 启用功能
// ...
}
```
### 方式 2: 临时测试(在浏览器控制台)
```javascript
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 测试红点显示
userStore.userInfo = {
...userStore.userInfo,
unread_count: 5 // 模拟 5 条未读消息
}
```
## 📊 数据流
```
┌─────────────────────────────────────────────────┐
│ 页面显示(useShow) │
│ userStore.fetchUserInfo() │
└──────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 后端接口返回用户信息 │
│ { │
│ user: { │
│ unread_count: 5, ← 红点字段 │
│ ... │
│ } │
│ } │
└──────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ User Store 计算红点状态 │
│ computed tabBarBadges() │
│ └─ 读取 features.tabbarBadge │
│ └─ 读取 userInfo.unread_count │
│ └─ 返回 ['me'] 或 [] │
└──────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ TabBar 组件 │
│ 自动读取 userStore.tabBarBadges │
│ 响应式更新红点显示 ✨ │
└─────────────────────────────────────────────────┘
```
## 🔄 刷新时机
### 自动刷新(已实现)
```javascript
// ✅ 首页 - 已添加
// pages/index/index.vue
useShow(() => {
if (userStore.isLoggedIn) {
userStore.fetchUserInfo()
}
})
```
### 手动刷新(可选)
```javascript
// 在其他页面需要时添加
import { useShow } from '@tarojs/taro'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
useShow(() => {
userStore.fetchUserInfo()
})
```
## 🎨 字段类型支持
### 数字类型(默认)
```javascript
// 用户信息
{
unread_count: 5 // 数字
}
// 配置
tabbarBadgeField: 'unread_count'
tabbarBadgeThreshold: 1 // >= 1 显示红点
```
### 布尔类型
```javascript
// 用户信息
{
has_notification: true // 布尔
}
// 配置
tabbarBadgeField: 'has_notification'
tabbarBadgeThreshold: 1 // 布尔类型无效
```
## 🔧 调整字段名称
当后端接口字段变化时,只需修改配置文件:
```javascript
// 场景 1: 字段改名
tabbarBadgeField: 'message_badge' // 从 'unread_count' 改名
// 场景 2: 改用布尔值
tabbarBadgeField: 'has_unread_message'
// 场景 3: 嵌套字段(需要扩展逻辑)
tabbarBadgeField: 'notification.unread.count'
```
## 🧪 测试方法
### 方法 1: 模拟数据
```javascript
// 在浏览器控制台
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 测试显示红点
userStore.userInfo = {
...userStore.userInfo,
unread_count: 5
}
// 测试隐藏红点
userStore.userInfo = {
...userStore.userInfo,
unread_count: 0
}
```
### 方法 2: 修改配置
```javascript
// 临时关闭功能
import { features } from '@/config/features'
features.tabbarBadge = false
// 临时开启功能
features.tabbarBadge = true
```
## 📋 上线检查清单
在正式上线前,确认:
- [ ] ✅ 功能开关已启用:`features.tabbarBadge = true`
- [ ] ✅ 字段名称正确:与后端接口返回字段一致
- [ ] ✅ 阈值设置合理:`tabbarBadgeThreshold`
- [ ] ✅ 关键页面已添加刷新逻辑(首页、我的)
- [ ] ✅ 红点显示正常测试通过
- [ ] ✅ 性能测试:频繁切换页面无卡顿
## 🎯 最佳实践
### 1. 防止频繁请求
```javascript
// ✅ 好:只在页面显示时请求
useShow(() => {
userStore.fetchUserInfo()
})
// ❌ 坏:使用定时器频繁请求
setInterval(() => {
userStore.fetchUserInfo()
}, 5000)
```
### 2. 条件刷新
```javascript
// ✅ 好:只在已登录时刷新
useShow(() => {
if (userStore.isLoggedIn) {
userStore.fetchUserInfo()
}
})
```
### 3. 错误处理
```javascript
// ✅ 好:捕获错误
useShow(() => {
userStore.fetchUserInfo().catch(err => {
console.error('刷新用户信息失败:', err)
})
})
```
## 🐛 常见问题
### Q: 红点不显示?
**检查清单**
1. 功能开关是否启用:`features.tabbarBadge === true`
2. 用户信息是否存在:`userStore.userInfo`
3. 字段值是否满足条件:`unread_count >= 1`
4. 是否已登录:`userStore.isLoggedIn === true`
### Q: 红点一直显示?
**原因**:后端返回的 `unread_count` 值 >= 1
**解决**
- 等待后端更新数据
- 或用户查看消息后,后端将字段值改为 0
### Q: 性能问题?
**优化**
- 减少刷新频率
- 添加防抖(500ms)
- 只在关键页面刷新
## 📚 相关文件
- 📄 `src/config/features.js` - 功能配置
- 📄 `src/stores/user.js` - 用户状态管理
- 📄 `src/components/TabBar.vue` - TabBar 组件
- 📄 `src/pages/index/index.vue` - 首页(已添加刷新逻辑)
## 🔄 更新日志
### 2026-02-03
- ✨ 新增 TabBar 红点提醒功能
- ✅ 添加功能开关配置
- ✅ 实现自动刷新逻辑
- 📝 创建使用文档
<!--
* @Date: 2026-01-29 20:33:23
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-30 21:03:18
* @LastEditTime: 2026-02-03 22:06:37
* @FilePath: /manulife-weapp/src/components/TabBar.vue
* @Description: 通用底部导航栏组件,用于页面底部固定导航栏,展示页面标题。
-->
......@@ -11,7 +11,11 @@
<view class="flex items-center justify-around py-[32rpx]">
<view class="flex-1 flex flex-col items-center justify-center" v-for="(item, index) in tabs" :key="index"
@tap="handleTabClick(item)">
<IconFont :name="item.icon" :class="[current === item.key ? 'text-blue-600' : 'text-gray-400']" size="24" />
<view class="relative">
<IconFont :name="item.icon" :class="[current === item.key ? 'text-blue-600' : 'text-gray-400']" size="24" />
<!-- 红点提醒标记 -->
<view v-if="badges.includes(item.key)" class="tabbar-badge"></view>
</view>
<text class="text-[20rpx] mt-[8rpx]" :class="[current === item.key ? 'text-blue-600' : 'text-gray-400']">{{
item.label }}</text>
</view>
......@@ -20,10 +24,11 @@
</template>
<script setup>
import { shallowRef } from 'vue'
import { shallowRef, computed } from 'vue'
import IconFont from '@/components/IconFont.vue'
import { useGo } from '@/hooks/useGo'
import Taro from '@tarojs/taro'
import { useUserStore } from '@/stores/user'
const props = defineProps({
current: {
......@@ -33,6 +38,10 @@ const props = defineProps({
})
const go = useGo()
const userStore = useUserStore()
// 自动从 user store 读取红点状态
const badges = computed(() => userStore.tabBarBadges)
const tabs = shallowRef([
{
......@@ -104,3 +113,19 @@ const handleTabClick = (item) => {
}
}
</script>
<style lang="less">
/* 红点提醒标记样式 */
.tabbar-badge {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 16rpx;
height: 16rpx;
background-color: #ff4d4f;
border-radius: 50%;
border: 2rpx solid #fff;
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.3);
z-index: 1;
}
</style>
......
/**
* 功能开关配置
*
* @description 用于控制功能的启用/禁用状态,方便灰度发布和功能回滚
* @module config/features
*/
/**
* 功能配置项
*
* @property {boolean} tabbarBadge - TabBar 红点提醒功能
* - true: 启用红点提醒(根据后端返回的 unread_count 判断)
* - false: 禁用红点提醒
*
* @property {string} tabbarBadgeField - 红点字段名称
* - 从用户信息接口读取该字段判断是否显示红点
* - 当前使用 'unread_count',后续可能根据实际接口调整
*
* @property {number} tabbarBadgeThreshold - 红点显示阈值
* - 当 unread_count >= 该值时显示红点
* - 默认为 1,即有未读消息时显示
*/
export const features = {
/**
* TabBar 红点提醒功能开关
*
* @type {boolean}
* @default false - 默认关闭,等接口字段确定后再开启
*/
tabbarBadge: false,
/**
* 红点字段名称
*
* @type {string}
* @default 'unread_count'
*
* @example
* // 如果后端返回不同字段,修改这里即可
* tabbarBadgeField: 'has_notification' // 布尔值
* tabbarBadgeField: 'unread_count' // 数字
* tabbarBadgeField: 'message_badge' // 对象
*/
tabbarBadgeField: 'unread_count',
/**
* 红点显示阈值
*
* @type {number}
* @default 1
*
* @description
* - 当字段为数字时:unread_count >= 1 显示红点
* - 当字段为布尔值时:此配置无效
*/
tabbarBadgeThreshold: 1
}
/**
* 检查功能是否启用
*
* @param {string} featureName - 功能名称
* @returns {boolean} 功能是否启用
*
* @example
* import { isFeatureEnabled } from '@/config/features'
*
* if (isFeatureEnabled('tabbarBadge')) {
* // 显示红点
* }
*/
export function isFeatureEnabled(featureName) {
return features[featureName] === true
}
/**
* 获取功能配置
*
* @param {string} featureName - 功能名称
* @returns {*} 功能配置值
*
* @example
* import { getFeatureConfig } from '@/config/features'
*
* const field = getFeatureConfig('tabbarBadgeField')
* console.log(field) // 'unread_count'
*/
export function getFeatureConfig(featureName) {
return features[featureName]
}
......@@ -164,10 +164,11 @@
<script setup>
import { ref, shallowRef } from 'vue';
import Taro, { useShareAppMessage, useLoad } from '@tarojs/taro';
import Taro, { useShareAppMessage, useLoad, useShow } from '@tarojs/taro';
import { useGo } from '@/hooks/useGo';
import { useListItemClick, ListType } from '@/composables/useListItemClick';
import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons';
import { useUserStore } from '@/stores/user';
import TabBar from '@/components/TabBar.vue';
import IconFont from '@/components/IconFont.vue';
import PlanPopup from '@/components/PlanPopup/index.vue';
......@@ -176,6 +177,9 @@ import SchemeB from '@/components/PlanSchemes/SchemeB.vue';
import ListItemActions from '@/components/ListItemActions/index.vue';
import { listAPI } from '@/api/get_product';
// User Store
const userStore = useUserStore();
// Plan Popup State
const showPlanPopup = ref(false);
const currentScheme = ref('A');
......@@ -337,6 +341,16 @@ useLoad(() => {
fetchHotProducts();
});
// 页面显示时刷新用户信息(更新 TabBar 红点状态)
useShow(() => {
// 只在已登录状态下刷新
if (userStore.isLoggedIn) {
userStore.fetchUserInfo().catch(err => {
console.error('刷新用户信息失败:', err);
});
}
});
useShareAppMessage(() => {
return {
title: '臻奇智荟圈',
......
......@@ -6,10 +6,11 @@
*/
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ref, computed } from 'vue'
import Taro from '@tarojs/taro'
import { loginStatusAPI, loginAPI, getProfileAPI, logoutAPI } from '@/api/user'
import { ensureOpenidAuthorized } from '@/utils/openid'
import { isFeatureEnabled, getFeatureConfig } from '@/config/features'
export const useUserStore = defineStore('user', () => {
// ========== 状态 ==========
......@@ -161,6 +162,57 @@ export const useUserStore = defineStore('user', () => {
}
}
/**
* TabBar 红点状态
*
* @description 根据 userInfo 中的字段计算是否显示红点
* - 只在功能开关启用时生效
* - 只处理 'me' 按钮的红点
* - 根据 unread_count 字段判断(可配置)
*
* @returns {string[]} 需要显示红点的 tab key 数组
*
* @example
* // 返回 ['me'] 表示在 '我的' 按钮显示红点
* // 返回 [] 表示不显示红点
*/
const tabBarBadges = computed(() => {
// 1. 检查功能开关
if (!isFeatureEnabled('tabbarBadge')) {
return []
}
// 2. 检查用户信息是否存在
if (!userInfo.value) {
return []
}
// 3. 获取配置的字段名和阈值
const fieldName = getFeatureConfig('tabbarBadgeField') // 'unread_count'
const threshold = getFeatureConfig('tabbarBadgeThreshold') // 1
// 4. 读取字段值
const fieldValue = userInfo.value[fieldName]
// 5. 判断是否显示红点
const badges = []
// 处理数字类型(如 unread_count: 5)
if (typeof fieldValue === 'number') {
if (fieldValue >= threshold) {
badges.push('me')
}
}
// 处理布尔类型(如 has_notification: true)
else if (typeof fieldValue === 'boolean') {
if (fieldValue) {
badges.push('me')
}
}
return badges
})
// ========== 返回 ==========
return {
// 状态
......@@ -169,6 +221,9 @@ export const useUserStore = defineStore('user', () => {
isLoggedIn,
loading,
// 计算属性
tabBarBadges,
// 方法
checkLoginStatus,
fetchUserInfo,
......