From be0dd750ebbbaf462214e8aaf1bc74b556985ce4 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Mon, 1 Dec 2025 16:56:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(vip):=20=E9=99=90=E5=88=B6=E5=BA=95?= =?UTF-8?q?=E9=83=A8=E6=A0=8F=E8=87=AA=E5=AE=9A=E4=B9=89=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E4=B8=BAVIP=E4=B8=93=E4=BA=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 非VIP用户尝试配置底部栏时将显示会员购买弹窗, 只有VIP会员才能自由开启或关闭导航标签。 包含会员权益说明的国际化支持和存储结构重构。 --- app.json | 2 +- app/settings/tab-bar-config.tsx | 49 ++++++++++++++++++++++-- components/model/MembershipModal.tsx | 14 +++++++ i18n/en/personal.ts | 1 + i18n/zh/personal.ts | 5 +++ ios/OutLive/Info.plist | 2 +- store/tabBarConfigSlice.ts | 56 +++++++++++++++++++--------- 7 files changed, 106 insertions(+), 23 deletions(-) diff --git a/app.json b/app.json index 7ff7456..4558aef 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Out Live", "slug": "digital-pilates", - "version": "1.1.3", + "version": "1.1.4", "orientation": "portrait", "scheme": "digitalpilates", "userInterfaceStyle": "light", diff --git a/app/settings/tab-bar-config.tsx b/app/settings/tab-bar-config.tsx index 9e1fc5e..0d2fed6 100644 --- a/app/settings/tab-bar-config.tsx +++ b/app/settings/tab-bar-config.tsx @@ -1,5 +1,6 @@ import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; +import { useVipService } from '@/hooks/useVipService'; import { resetToDefault, selectTabBarConfigs, @@ -9,7 +10,7 @@ import { import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Alert, ScrollView, @@ -20,6 +21,7 @@ import { View } from 'react-native'; +import { MembershipModal } from '@/components/model/MembershipModal'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { IconSymbol } from '@/components/ui/IconSymbol'; import { palette } from '@/constants/Colors'; @@ -31,15 +33,38 @@ export default function TabBarConfigScreen() { const dispatch = useAppDispatch(); const safeAreaTop = useSafeAreaTop(60); const configs = useAppSelector(selectTabBarConfigs); + const { isVip } = useVipService(); + const [showMembershipModal, setShowMembershipModal] = useState(false); // 处理开关切换 const handleToggle = useCallback( (tabId: string) => { - dispatch(toggleTabEnabled(tabId)); + // 直接检查用户是否是 VIP(底部栏配置不是权益类功能,而是基础功能) + if (isVip) { + // VIP 用户可以正常切换 + dispatch(toggleTabEnabled(tabId)); + } else { + // 非 VIP 用户显示购买弹窗 + setShowMembershipModal(true); + } }, - [dispatch] + [dispatch, isVip] ); + // 页面加载时检查 VIP 状态 + useEffect(() => { + if (!isVip) { + // 非 VIP 用户进入页面时立即显示购买弹窗 + setShowMembershipModal(true); + } + }, [isVip]); + + // 购买成功回调 + const handlePurchaseSuccess = useCallback(() => { + // 购买成功后可以执行一些操作,比如刷新用户信息 + console.log('会员购买成功'); + }, []); + // 恢复默认设置 const handleReset = useCallback(() => { Alert.alert( @@ -80,6 +105,11 @@ export default function TabBarConfigScreen() { {t('personal.tabBarConfig.cannotDisable')} )} + {item.canBeDisabled && !isVip && ( + + {t('personal.tabBarConfig.vipOnly')} + + )} @@ -87,7 +117,7 @@ export default function TabBarConfigScreen() { handleToggle(item.id)} - disabled={!item.canBeDisabled} + disabled={!item.canBeDisabled || !isVip} trackColor={{ false: '#E5E5E5', true: '#9370DB' }} thumbColor="#FFFFFF" style={styles.switch} @@ -153,6 +183,13 @@ export default function TabBarConfigScreen() { + + {/* 会员购买弹窗 */} + setShowMembershipModal(false)} + onPurchaseSuccess={handlePurchaseSuccess} + /> ); } @@ -253,6 +290,10 @@ const styles = StyleSheet.create({ fontSize: 12, color: '#9370DB', }, + vipSubtitle: { + fontSize: 12, + color: '#FF6B6B', + }, switch: { transform: [{ scaleX: 0.9 }, { scaleY: 0.9 }], }, diff --git a/components/model/MembershipModal.tsx b/components/model/MembershipModal.tsx index 5cb002d..e315f1d 100644 --- a/components/model/MembershipModal.tsx +++ b/components/model/MembershipModal.tsx @@ -194,6 +194,20 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members vipText: t('membershipModal.benefits.permissions.notSupported') } }, + { + title: t('membershipModal.benefits.items.tabBarCustomization.title'), + description: t('membershipModal.benefits.items.tabBarCustomization.description'), + vip: { + type: 'exclusive', + text: t('membershipModal.benefits.permissions.fullSupport'), + vipText: t('membershipModal.benefits.permissions.unlimited') + }, + regular: { + type: 'exclusive', + text: t('membershipModal.benefits.permissions.notSupported'), + vipText: t('membershipModal.benefits.permissions.notSupported') + } + }, ]; // 根据选中的产品生成tips内容 diff --git a/i18n/en/personal.ts b/i18n/en/personal.ts index c3a1783..d21dd03 100644 --- a/i18n/en/personal.ts +++ b/i18n/en/personal.ts @@ -92,6 +92,7 @@ export const personal = { description: 'Use toggles to show or hide tabs', resetButton: 'Reset', cannotDisable: 'Cannot be disabled', + vipOnly: 'VIP members only', resetConfirm: { title: 'Reset to Default?', message: 'This will reset all tab bar settings and visibility', diff --git a/i18n/zh/personal.ts b/i18n/zh/personal.ts index 1312479..7777a4c 100644 --- a/i18n/zh/personal.ts +++ b/i18n/zh/personal.ts @@ -92,6 +92,7 @@ export const personal = { description: '使用开关控制标签的显示和隐藏', resetButton: '恢复默认', cannotDisable: '此标签不可关闭', + vipOnly: '仅限VIP会员', resetConfirm: { title: '恢复默认设置?', message: '将重置所有底部栏配置和显示状态', @@ -299,6 +300,10 @@ export const membershipModal = { title: '解锁无限自定义挑战', description: '突破限制,邀请挚友同行,让坚持不再孤单,共同见证蜕变', }, + tabBarCustomization: { + title: '底部栏自定义', + description: '个性化底部导航栏,隐藏不需要的功能标签', + }, }, permissions: { unlimited: '无限次使用', diff --git a/ios/OutLive/Info.plist b/ios/OutLive/Info.plist index bdb8f7e..e38d87e 100644 --- a/ios/OutLive/Info.plist +++ b/ios/OutLive/Info.plist @@ -27,7 +27,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.1.3 + 1.1.4 CFBundleSignature ???? CFBundleURLTypes diff --git a/store/tabBarConfigSlice.ts b/store/tabBarConfigSlice.ts index 8ffa78f..b7eafb7 100644 --- a/store/tabBarConfigSlice.ts +++ b/store/tabBarConfigSlice.ts @@ -9,10 +9,17 @@ export interface TabConfig { icon: string; // SF Symbol 图标名 titleKey: string; // i18n 翻译 key enabled: boolean; // 是否启用 - canBeDisabled: boolean; // 是否可以被禁用 + canBeDisabled: boolean; // 是否可以被禁用(系统级配置,不持久化) order: number; // 显示顺序 } +// 用户可持久化的配置(只包含用户可控制的属性) +interface UserTabConfig { + id: string; + enabled: boolean; + order: number; +} + // State 接口 interface TabBarConfigState { configs: TabConfig[]; @@ -34,7 +41,7 @@ export const DEFAULT_TAB_CONFIGS: TabConfig[] = [ icon: 'pills.fill', titleKey: 'statistics.tabs.medications', enabled: true, - canBeDisabled: false, + canBeDisabled: true, // 用药管理可以被关闭 order: 2, }, { @@ -42,7 +49,7 @@ export const DEFAULT_TAB_CONFIGS: TabConfig[] = [ icon: 'timer', titleKey: 'statistics.tabs.fasting', enabled: true, - canBeDisabled: true, // 只有断食可以被关闭 + canBeDisabled: true, // 断食可以被关闭 order: 3, }, { @@ -50,7 +57,7 @@ export const DEFAULT_TAB_CONFIGS: TabConfig[] = [ icon: 'trophy.fill', titleKey: 'statistics.tabs.challenges', enabled: true, - canBeDisabled: false, + canBeDisabled: true, // 挑战可以被关闭 order: 4, }, { @@ -120,10 +127,16 @@ const tabBarConfigSlice = createSlice({ }, }); -// 持久化配置到 AsyncStorage +// 持久化配置到 AsyncStorage(只保存用户可控制的属性) const saveConfigsToStorage = async (configs: TabConfig[]) => { try { - await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(configs)); + // 只保存用户可控制的属性:enabled 和 order + const userConfigs: UserTabConfig[] = configs.map(config => ({ + id: config.id, + enabled: config.enabled, + order: config.order, + })); + await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(userConfigs)); logger.info('底部栏配置已保存'); } catch (error) { logger.error('保存底部栏配置失败:', error); @@ -136,12 +149,12 @@ export const loadTabBarConfigs = () => async (dispatch: any) => { const stored = await AsyncStorage.getItem(STORAGE_KEY); if (stored) { - const configs = JSON.parse(stored) as TabConfig[]; + const userConfigs = JSON.parse(stored) as UserTabConfig[]; // 验证配置有效性 - if (Array.isArray(configs) && configs.length > 0) { - // 合并默认配置,确保新增的 tab 也能显示 - const mergedConfigs = mergeWithDefaults(configs); + if (Array.isArray(userConfigs) && userConfigs.length > 0) { + // 合并用户配置和默认配置 + const mergedConfigs = mergeWithDefaults(userConfigs); dispatch(setConfigs(mergedConfigs)); logger.info('底部栏配置已加载'); return; @@ -159,15 +172,24 @@ export const loadTabBarConfigs = () => async (dispatch: any) => { } }; -// 合并存储的配置和默认配置 -const mergeWithDefaults = (storedConfigs: TabConfig[]): TabConfig[] => { - const merged = [...storedConfigs]; +// 合并用户配置和默认配置 +const mergeWithDefaults = (userConfigs: UserTabConfig[]): TabConfig[] => { + const merged: TabConfig[] = []; - // 检查是否有新增的默认 tab + // 遍历默认配置,将用户的 enabled 和 order 合并进来 DEFAULT_TAB_CONFIGS.forEach(defaultConfig => { - const exists = merged.find(c => c.id === defaultConfig.id); - if (!exists) { - // 新增的 tab,添加到末尾 + const userConfig = userConfigs.find(c => c.id === defaultConfig.id); + + if (userConfig) { + // 合并:系统配置(icon, titleKey, canBeDisabled)从默认配置读取 + // 用户配置(enabled, order)从用户配置读取 + merged.push({ + ...defaultConfig, + enabled: userConfig.enabled, + order: userConfig.order, + }); + } else { + // 新增的 tab,使用默认配置 merged.push({ ...defaultConfig, order: merged.length + 1,