feat(membership): 重构会员权益对比表并优化购买界面布局

- 重构权益对比数据结构,支持独占、有限、无限三种权限类型
- 新增权限图标显示逻辑,区分VIP和普通用户权限状态
- 优化会员卡片布局,采用三段式布局提升视觉效果
- 实现悬浮购买按钮,支持Liquid Glass毛玻璃效果
- 增强购买流程验证,添加自动选择产品和详细错误处理
- 调整界面间距和样式,提升整体用户体验
This commit is contained in:
richarjiang
2025-10-27 14:43:19 +08:00
parent 82edb2593c
commit db8b50f6d7

View File

@@ -74,13 +74,96 @@ const DEFAULT_PLANS: MembershipPlan[] = [
}, },
]; ];
// 权限类型枚举
type PermissionType = 'exclusive' | 'limited' | 'unlimited';
const BENEFIT_COMPARISON = [ // 权限配置接口
{ title: 'AI拍照记录热量', vip: true, regular: false }, interface PermissionConfig {
{ title: 'AI拍照识别包装', vip: true, regular: false }, type: PermissionType;
{ title: '私人饮食建议', vip: true, regular: false }, text: string;
{ title: '定制健身训练', vip: true, regular: false }, vipText?: string; // VIP用户的特殊文案可选
{ title: '每日健康提醒', vip: true, regular: true }, }
// 权益对比项接口
interface BenefitItem {
title: string;
description?: string; // 功能描述,可选
vip: PermissionConfig;
regular: PermissionConfig;
}
// 权益对比配置
const BENEFIT_COMPARISON: BenefitItem[] = [
{
title: 'AI拍照记录热量',
description: '通过拍照识别食物并自动记录热量',
vip: {
type: 'unlimited',
text: '无限次使用',
vipText: '无限次使用'
},
regular: {
type: 'limited',
text: '有限次使用',
vipText: '每日3次'
}
},
{
title: 'AI拍照识别包装',
description: '识别食品包装上的营养成分信息',
vip: {
type: 'unlimited',
text: '无限次使用',
vipText: '无限次使用'
},
regular: {
type: 'limited',
text: '有限次使用',
vipText: '每日5次'
}
},
{
title: '每日健康提醒',
description: '根据个人目标提供个性化健康提醒',
vip: {
type: 'unlimited',
text: '完全支持',
vipText: '智能提醒'
},
regular: {
type: 'unlimited',
text: '基础提醒',
vipText: '基础提醒'
}
},
{
title: 'AI教练对话',
description: '与AI健康教练进行个性化对话咨询',
vip: {
type: 'unlimited',
text: '无限次对话',
vipText: '深度分析'
},
regular: {
type: 'limited',
text: '有限次对话',
vipText: '每日10次'
}
},
{
title: '体态评估',
description: '通过照片分析体态问题并提供改善建议',
vip: {
type: 'exclusive',
text: '完全支持',
vipText: '专业评估'
},
regular: {
type: 'exclusive',
text: '不可使用',
vipText: '不可使用'
}
}
]; ];
const PLAN_STYLE_CONFIG: Record<MembershipPlan['type'], { gradient: readonly [string, string]; accent: string }> = { const PLAN_STYLE_CONFIG: Record<MembershipPlan['type'], { gradient: readonly [string, string]; accent: string }> = {
@@ -98,6 +181,29 @@ const PLAN_STYLE_CONFIG: Record<MembershipPlan['type'], { gradient: readonly [st
}, },
}; };
// 根据权限类型获取对应的图标
const getPermissionIcon = (type: PermissionType, isVip: boolean) => {
switch (type) {
case 'exclusive':
return isVip ? (
<Ionicons name="checkmark-circle" size={20} color="#FFB200" />
) : (
<Ionicons name="remove" size={20} color="#D1D4DA" />
);
case 'limited':
return isVip ? (
<Ionicons name="checkmark-circle" size={20} color="#FFB200" />
) : (
<Ionicons name="time-outline" size={20} color="#8E8E93" />
);
case 'unlimited':
return (
<Ionicons name="checkmark-circle" size={20} color={isVip ? "#FFB200" : "#8E8E93"} />
);
default:
return <Ionicons name="remove" size={20} color="#D1D4DA" />;
}
};
export function MembershipModal({ visible, onClose, onPurchaseSuccess }: MembershipModalProps) { export function MembershipModal({ visible, onClose, onPurchaseSuccess }: MembershipModalProps) {
const [selectedProduct, setSelectedProduct] = useState<PurchasesStoreProduct | null>(null); const [selectedProduct, setSelectedProduct] = useState<PurchasesStoreProduct | null>(null);
@@ -215,7 +321,14 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// 获取产品后,检查用户的购买记录并自动选中对应套餐 // 获取产品后,检查用户的购买记录并自动选中对应套餐
await checkAndSelectActivePlan(productsToUse); await checkAndSelectActivePlan(productsToUse);
setSelectedProduct(current => current ?? (productsToUse[0] ?? null)); // 确保始终有一个选中的产品
setSelectedProduct(current => {
// 如果已经有选中的产品,保持不变
if (current) return current;
// 否则选择第一个可用产品
return productsToUse[0] ?? null;
});
} catch (e: any) { } catch (e: any) {
// 安全地处理错误对象,避免循环引用 // 安全地处理错误对象,避免循环引用
const errorData = { const errorData = {
@@ -431,6 +544,15 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
const handlePurchase = async () => { const handlePurchase = async () => {
// 添加调试日志
log.info('handlePurchase 被调用', {
loading,
productsLength: products.length,
selectedProductId: selectedProduct?.identifier,
selectedProductTitle: selectedProduct?.title,
agreementAccepted
});
// 验证是否已同意协议 // 验证是否已同意协议
if (!agreementAccepted) { if (!agreementAccepted) {
Alert.alert( Alert.alert(
@@ -446,6 +568,16 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
return; return;
} }
// 如果没有选中的产品但有可用产品,自动选择第一个
if (!selectedProduct && products.length > 0) {
log.info('自动选择第一个可用产品', {
firstProductId: products[0]?.identifier,
firstProductTitle: products[0]?.title
});
setSelectedProduct(products[0]);
return; // 返回让用户确认选择后再点击
}
// 验证是否选择了产品 // 验证是否选择了产品
if (!selectedProduct) { if (!selectedProduct) {
Alert.alert( Alert.alert(
@@ -740,20 +872,27 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
end={{ x: 1, y: 1 }} end={{ x: 1, y: 1 }}
style={styles.planCardGradient} style={styles.planCardGradient}
> >
{plan.tag && ( <View style={styles.planCardTopSection}>
<View style={styles.planTag}> {plan.tag && (
<Text style={styles.planTagText}>{plan.tag}</Text> <View style={styles.planTag}>
</View> <Text style={styles.planTagText}>{plan.tag}</Text>
)} </View>
)}
<Text style={styles.planCardTitle}>{displayTitle}</Text>
</View>
<Text style={styles.planCardTitle}>{displayTitle}</Text> <View style={styles.planCardMiddleSection}>
<Text style={[styles.planCardPrice, styleConfig && { color: styleConfig.accent }]}> <Text style={[styles.planCardPrice, styleConfig && { color: styleConfig.accent }]}>
{priceLabel || '--'} {priceLabel || '--'}
</Text> </Text>
{plan.originalPrice && ( {plan.originalPrice && (
<Text style={styles.planCardOriginalPrice}>{plan.originalPrice}</Text> <Text style={styles.planCardOriginalPrice}>{plan.originalPrice}</Text>
)} )}
<Text style={styles.planCardDescription}>{plan.subtitle}</Text> </View>
<View style={styles.planCardBottomSection}>
<Text style={styles.planCardDescription}>{plan.subtitle}</Text>
</View>
</LinearGradient> </LinearGradient>
</TouchableOpacity> </TouchableOpacity>
); );
@@ -807,7 +946,9 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
<View style={styles.sectionCard}> <View style={[styles.sectionCard, {
marginTop: 56
}]}>
<View style={styles.sectionTitleRow}> <View style={styles.sectionTitleRow}>
<View style={styles.sectionTitleBadge}> <View style={styles.sectionTitleBadge}>
<Ionicons name="star" size={16} color="#7B2CBF" /> <Ionicons name="star" size={16} color="#7B2CBF" />
@@ -860,20 +1001,27 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
index % 2 === 1 && styles.tableRowAlt, index % 2 === 1 && styles.tableRowAlt,
]} ]}
> >
<Text style={[styles.tableCellText, styles.tableTitleCell]}>{row.title}</Text> <View style={styles.tableTitleCell}>
<View style={styles.tableVipCell}> <Text style={styles.tableCellText}>{row.title}</Text>
{row.vip ? ( {row.description && (
<Ionicons name="checkmark-circle" size={20} color="#FFB200" /> <Text style={styles.tableDescriptionText}>{row.description}</Text>
) : (
<Ionicons name="remove" size={20} color="#D1D4DA" />
)} )}
</View> </View>
{/* VIP 权限列 */}
<View style={styles.tableVipCell}>
<View style={styles.permissionContainer}>
{getPermissionIcon(row.vip.type, true)}
<Text style={styles.permissionText}>{row.vip.vipText || row.vip.text}</Text>
</View>
</View>
{/* 普通用户权限列 */}
<View style={styles.tableNormalCell}> <View style={styles.tableNormalCell}>
{row.regular ? ( <View style={styles.permissionContainer}>
<Ionicons name="checkmark-circle" size={20} color="#8E8E93" /> {getPermissionIcon(row.regular.type, false)}
) : ( <Text style={styles.permissionText}>{row.regular.vipText || row.regular.text}</Text>
<Ionicons name="remove" size={20} color="#D1D4DA" /> </View>
)}
</View> </View>
</View> </View>
))} ))}
@@ -881,38 +1029,6 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
</View> </View>
<View style={styles.bottomSection}> <View style={styles.bottomSection}>
<View style={styles.noticeBanner}>
<Text style={styles.noticeText}>667+</Text>
</View>
<TouchableOpacity
style={[
styles.purchaseButton,
(loading || !selectedProduct || !agreementAccepted) && styles.disabledButton
]}
onPress={handlePurchase}
disabled={loading || !selectedProduct || !agreementAccepted}
accessible={true}
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
accessibilityHint={
loading
? '购买正在进行中,请稍候'
: selectedProduct
? `点击购买${selectedProduct.title || '已选'}会员套餐`
: '请选择会员套餐后再进行购买'
}
accessibilityState={{ disabled: loading || !selectedProduct || !agreementAccepted }}
>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
<Text style={styles.purchaseButtonText}>...</Text>
</View>
) : (
<Text style={styles.purchaseButtonText}></Text>
)}
</TouchableOpacity>
<View style={styles.agreementRow}> <View style={styles.agreementRow}>
<CustomCheckBox <CustomCheckBox
checked={agreementAccepted} checked={agreementAccepted}
@@ -964,6 +1080,83 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</ScrollView> </ScrollView>
{/* 悬浮购买按钮 */}
<View style={styles.floatingPurchaseContainer}>
{isLiquidGlassAvailable() ? (
<GlassView
style={[
styles.purchaseButton,
(loading || products.length === 0 || !agreementAccepted) && styles.disabledButton
]}
glassEffectStyle="clear"
tintColor="rgba(21, 21, 21, 0.8)"
isInteractive={true}
>
<TouchableOpacity
onPress={handlePurchase}
disabled={loading || products.length === 0 || !agreementAccepted}
accessible={true}
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
accessibilityHint={
loading
? '购买正在进行中,请稍候'
: products.length === 0
? '正在加载会员套餐,请稍候'
: !selectedProduct
? '请选择会员套餐后再进行购买'
: `点击购买${selectedProduct.title || '已选'}会员套餐`
}
accessibilityState={{ disabled: loading || products.length === 0 || !agreementAccepted }}
style={styles.purchaseButtonContent}
>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
<Text style={styles.purchaseButtonText}>...</Text>
</View>
) : (
<Text style={styles.purchaseButtonText}></Text>
)}
</TouchableOpacity>
</GlassView>
) : (
<View
style={[
styles.purchaseButton,
styles.fallbackPurchaseButton,
(loading || products.length === 0 || !agreementAccepted) && styles.disabledButton
]}
>
<TouchableOpacity
onPress={handlePurchase}
disabled={loading || products.length === 0 || !agreementAccepted}
accessible={true}
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
accessibilityHint={
loading
? '购买正在进行中,请稍候'
: products.length === 0
? '正在加载会员套餐,请稍候'
: !selectedProduct
? '请选择会员套餐后再进行购买'
: `点击购买${selectedProduct.title || '已选'}会员套餐`
}
accessibilityState={{ disabled: loading || products.length === 0 || !agreementAccepted }}
style={styles.purchaseButtonContent}
>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
<Text style={styles.purchaseButtonText}>...</Text>
</View>
) : (
<Text style={styles.purchaseButtonText}></Text>
)}
</TouchableOpacity>
</View>
)}
</View>
</View> </View>
</View> </View>
</Modal> </Modal>
@@ -991,7 +1184,7 @@ const styles = StyleSheet.create({
}, },
modalContentContainer: { modalContentContainer: {
paddingHorizontal: 20, paddingHorizontal: 20,
paddingBottom: 32, paddingBottom: 100, // 增加底部内边距,避免内容被悬浮按钮遮挡
paddingTop: 16, paddingTop: 16,
}, },
floatingBackButton: { floatingBackButton: {
@@ -1080,9 +1273,11 @@ const styles = StyleSheet.create({
elevation: 4, elevation: 4,
}, },
planCardGradient: { planCardGradient: {
flex: 1,
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 18, paddingVertical: 18,
minHeight: 170, minHeight: 170,
justifyContent: 'space-between',
}, },
planTag: { planTag: {
alignSelf: 'flex-start', alignSelf: 'flex-start',
@@ -1111,14 +1306,26 @@ const styles = StyleSheet.create({
fontSize: 13, fontSize: 13,
color: '#8E8EA1', color: '#8E8EA1',
textDecorationLine: 'line-through', textDecorationLine: 'line-through',
marginTop: 4, marginTop: 2,
}, },
planCardDescription: { planCardDescription: {
fontSize: 12, fontSize: 12,
color: '#6C6C77', color: '#6C6C77',
marginTop: 12,
lineHeight: 17, lineHeight: 17,
}, },
planCardTopSection: {
flex: 1,
justifyContent: 'flex-start',
},
planCardMiddleSection: {
flex: 1,
justifyContent: 'center',
alignItems: 'flex-start',
},
planCardBottomSection: {
flex: 1,
justifyContent: 'flex-end',
},
tipsContainer: { tipsContainer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@@ -1163,15 +1370,18 @@ const styles = StyleSheet.create({
color: '#3E3E44', color: '#3E3E44',
}, },
tableTitleCell: { tableTitleCell: {
flex: 1.3, flex: 1.5,
justifyContent: 'center',
}, },
tableVipCell: { tableVipCell: {
flex: 0.8, flex: 0.8,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
}, },
tableNormalCell: { tableNormalCell: {
flex: 0.8, flex: 0.8,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
}, },
tableRowAlt: { tableRowAlt: {
backgroundColor: '#FBFBFF', backgroundColor: '#FBFBFF',
@@ -1188,26 +1398,27 @@ const styles = StyleSheet.create({
shadowOffset: { width: 0, height: 6 }, shadowOffset: { width: 0, height: 6 },
elevation: 1, elevation: 1,
}, },
noticeBanner: {
backgroundColor: '#FFE7E0',
borderRadius: 12,
paddingVertical: 10,
paddingHorizontal: 16,
marginBottom: 16,
},
noticeText: {
color: '#D35400',
fontSize: 13,
textAlign: 'center',
fontWeight: '600',
},
purchaseButton: { purchaseButton: {
backgroundColor: '#151515',
borderRadius: 28, borderRadius: 28,
height: 52, height: 52,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginBottom: 16, overflow: 'hidden',
},
purchaseButtonContent: {
flex: 1,
width: '100%',
justifyContent: 'center',
alignItems: 'center',
},
floatingPurchaseContainer: {
position: 'absolute',
bottom: 34, // 底部安全区域
left: 20,
right: 20,
},
fallbackPurchaseButton: {
backgroundColor: '#151515',
}, },
disabledButton: { disabledButton: {
backgroundColor: '#C6C6C8', backgroundColor: '#C6C6C8',
@@ -1228,23 +1439,23 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
flexWrap: 'wrap', flexWrap: 'nowrap',
marginBottom: 16, marginBottom: 16,
}, },
agreementPrefix: { agreementPrefix: {
fontSize: 11, fontSize: 10,
color: '#666672', color: '#666672',
marginHorizontal: 6, marginRight: 4,
}, },
agreementLink: { agreementLink: {
fontSize: 11, fontSize: 10,
color: '#E91E63', color: '#E91E63',
textDecorationLine: 'underline', textDecorationLine: 'underline',
fontWeight: '500', fontWeight: '500',
marginHorizontal: 4, marginHorizontal: 2,
}, },
agreementSeparator: { agreementSeparator: {
fontSize: 11, fontSize: 10,
color: '#A0A0B0', color: '#A0A0B0',
marginHorizontal: 2, marginHorizontal: 2,
}, },
@@ -1271,4 +1482,24 @@ const styles = StyleSheet.create({
disabledPlanCard: { disabledPlanCard: {
opacity: 0.5, opacity: 0.5,
}, },
// 新增样式:权限相关
tableDescriptionText: {
fontSize: 11,
color: '#8E8E93',
marginTop: 2,
lineHeight: 14,
},
permissionContainer: {
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
paddingVertical: 4,
},
permissionText: {
fontSize: 10,
color: '#6B6B73',
marginTop: 4,
textAlign: 'center',
lineHeight: 12,
},
}); });