feat(membership): 重构会员系统架构并优化VIP卡片显示
- 创建独立的会员服务模块 services/membership.ts,统一管理会员计划元数据和工具函数 - 新增 membershipSlice Redux状态管理,集中处理会员数据和状态 - 重构个人中心VIP会员卡片,支持动态显示会员计划和有效期 - 优化会员购买弹窗,使用统一的会员计划配置 - 改进会员数据获取流程,确保状态同步和一致性
This commit is contained in:
@@ -6,12 +6,14 @@ import { useMembershipModal } from '@/contexts/MembershipModalContext';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
import { selectActiveMembershipPlanName } from '@/store/membershipSlice';
|
||||
import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice';
|
||||
import { getItem, setItem } from '@/utils/kvStore';
|
||||
import { log } from '@/utils/logger';
|
||||
import { getNotificationEnabled, setNotificationEnabled as saveNotificationEnabled } from '@/utils/userPreferences';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import dayjs from 'dayjs';
|
||||
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||
import { Image } from 'expo-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
@@ -57,6 +59,7 @@ export default function PersonalScreen() {
|
||||
|
||||
// 直接使用 Redux 中的用户信息,避免重复状态管理
|
||||
const userProfile = useAppSelector((state) => state.user.profile);
|
||||
const activeMembershipPlanName = useAppSelector(selectActiveMembershipPlanName);
|
||||
|
||||
|
||||
// 页面聚焦时获取最新用户信息
|
||||
@@ -270,6 +273,72 @@ export default function PersonalScreen() {
|
||||
</View>
|
||||
);
|
||||
|
||||
const VipMembershipCard = () => {
|
||||
const fallbackProfile = userProfile as Record<string, unknown>;
|
||||
const fallbackExpire = ['membershipExpiration', 'vipExpiredAt', 'vipExpiresAt', 'vipExpireDate']
|
||||
.map((key) => fallbackProfile[key])
|
||||
.find((value): value is string => typeof value === 'string' && value.trim().length > 0);
|
||||
|
||||
const rawExpireDate = userProfile.membershipExpiration
|
||||
|
||||
let formattedExpire = '长期有效';
|
||||
if (typeof rawExpireDate === 'string' && rawExpireDate.trim().length > 0) {
|
||||
const parsed = dayjs(rawExpireDate);
|
||||
formattedExpire = parsed.isValid() ? parsed.format('YYYY年MM月DD日') : rawExpireDate;
|
||||
}
|
||||
|
||||
const planName =
|
||||
activeMembershipPlanName?.trim() ||
|
||||
userProfile.vipPlanName?.trim() ||
|
||||
'VIP 会员';
|
||||
|
||||
return (
|
||||
<View style={styles.sectionContainer}>
|
||||
<LinearGradient
|
||||
colors={['#5B4CFF', '#8D5BEA']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.vipCard}
|
||||
>
|
||||
<View style={styles.vipCardDecorationLarge} />
|
||||
<View style={styles.vipCardDecorationSmall} />
|
||||
<View style={styles.vipCardHeader}>
|
||||
<View style={styles.vipCardHeaderLeft}>
|
||||
<View style={styles.vipBadge}>
|
||||
<Ionicons name="sparkles-outline" size={16} color="#FFD361" />
|
||||
<Text style={styles.vipBadgeText}>尊享会员</Text>
|
||||
</View>
|
||||
<Text style={styles.vipCardTitle}>{planName}</Text>
|
||||
</View>
|
||||
<View style={styles.vipCardIllustration}>
|
||||
<Ionicons name="ribbon" size={32} color="rgba(255,255,255,0.88)" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.vipCardFooter}>
|
||||
<View style={styles.vipExpiryInfo}>
|
||||
<Text style={styles.vipExpiryLabel}>会员有效期</Text>
|
||||
<View style={styles.vipExpiryRow}>
|
||||
<Ionicons name="time-outline" size={16} color="rgba(255,255,255,0.85)" />
|
||||
<Text style={styles.vipExpiryValue}>{formattedExpire}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.vipChangeButton}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => {
|
||||
void handleMembershipPress();
|
||||
}}
|
||||
>
|
||||
<Ionicons name="swap-horizontal-outline" size={16} color="#2F1767" />
|
||||
<Text style={styles.vipChangeButtonText}>更改会员套餐</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// 数据统计部分
|
||||
const StatsSection = () => (
|
||||
<View style={styles.sectionContainer}>
|
||||
@@ -427,7 +496,7 @@ export default function PersonalScreen() {
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<UserHeader />
|
||||
{userProfile.isVip ? null : <MembershipBanner />}
|
||||
{userProfile.isVip ? <VipMembershipCard /> : <MembershipBanner />}
|
||||
<StatsSection />
|
||||
<View style={styles.fishRecordContainer}>
|
||||
{/* <Image
|
||||
@@ -509,6 +578,116 @@ const styles = StyleSheet.create({
|
||||
height: 180,
|
||||
borderRadius: 16,
|
||||
},
|
||||
vipCard: {
|
||||
borderRadius: 20,
|
||||
padding: 20,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#4C3AFF',
|
||||
shadowOffset: { width: 0, height: 12 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 20,
|
||||
elevation: 6,
|
||||
},
|
||||
vipCardDecorationLarge: {
|
||||
position: 'absolute',
|
||||
right: -40,
|
||||
top: -30,
|
||||
width: 160,
|
||||
height: 160,
|
||||
borderRadius: 80,
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
},
|
||||
vipCardDecorationSmall: {
|
||||
position: 'absolute',
|
||||
left: -30,
|
||||
bottom: -30,
|
||||
width: 140,
|
||||
height: 140,
|
||||
borderRadius: 70,
|
||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||
},
|
||||
vipCardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
vipCardHeaderLeft: {
|
||||
flex: 1,
|
||||
paddingRight: 16,
|
||||
},
|
||||
vipBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: 'rgba(255,255,255,0.18)',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 14,
|
||||
},
|
||||
vipBadgeText: {
|
||||
color: '#FFD361',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 4,
|
||||
},
|
||||
vipCardTitle: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
marginTop: 12,
|
||||
},
|
||||
vipCardSubtitle: {
|
||||
color: 'rgba(255,255,255,0.88)',
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
marginTop: 6,
|
||||
},
|
||||
vipCardIllustration: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
backgroundColor: 'rgba(255,255,255,0.18)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
vipCardFooter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 24,
|
||||
},
|
||||
vipExpiryInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
vipExpiryLabel: {
|
||||
color: 'rgba(255,255,255,0.72)',
|
||||
fontSize: 12,
|
||||
marginBottom: 6,
|
||||
},
|
||||
vipExpiryRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
vipExpiryValue: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
},
|
||||
vipChangeButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 22,
|
||||
},
|
||||
vipChangeButtonText: {
|
||||
color: '#2F1767',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
},
|
||||
// 用户信息区域
|
||||
userInfoContainer: {
|
||||
flexDirection: 'row',
|
||||
|
||||
Reference in New Issue
Block a user