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 },
{ title: 'AI拍照识别包装', vip: true, regular: false },
{ title: '私人饮食建议', vip: true, regular: false },
{ title: '定制健身训练', vip: true, regular: false },
{ title: '每日健康提醒', vip: true, regular: true },
// 权限配置接口
interface PermissionConfig {
type: PermissionType;
text: string;
vipText?: string; // VIP用户的特殊文案可选
}
// 权益对比项接口
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 }> = {
@@ -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) {
const [selectedProduct, setSelectedProduct] = useState<PurchasesStoreProduct | null>(null);
@@ -215,7 +321,14 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
// 获取产品后,检查用户的购买记录并自动选中对应套餐
await checkAndSelectActivePlan(productsToUse);
setSelectedProduct(current => current ?? (productsToUse[0] ?? null));
// 确保始终有一个选中的产品
setSelectedProduct(current => {
// 如果已经有选中的产品,保持不变
if (current) return current;
// 否则选择第一个可用产品
return productsToUse[0] ?? null;
});
} catch (e: any) {
// 安全地处理错误对象,避免循环引用
const errorData = {
@@ -431,6 +544,15 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
const handlePurchase = async () => {
// 添加调试日志
log.info('handlePurchase 被调用', {
loading,
productsLength: products.length,
selectedProductId: selectedProduct?.identifier,
selectedProductTitle: selectedProduct?.title,
agreementAccepted
});
// 验证是否已同意协议
if (!agreementAccepted) {
Alert.alert(
@@ -446,6 +568,16 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
return;
}
// 如果没有选中的产品但有可用产品,自动选择第一个
if (!selectedProduct && products.length > 0) {
log.info('自动选择第一个可用产品', {
firstProductId: products[0]?.identifier,
firstProductTitle: products[0]?.title
});
setSelectedProduct(products[0]);
return; // 返回让用户确认选择后再点击
}
// 验证是否选择了产品
if (!selectedProduct) {
Alert.alert(
@@ -740,20 +872,27 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
end={{ x: 1, y: 1 }}
style={styles.planCardGradient}
>
{plan.tag && (
<View style={styles.planTag}>
<Text style={styles.planTagText}>{plan.tag}</Text>
</View>
)}
<View style={styles.planCardTopSection}>
{plan.tag && (
<View style={styles.planTag}>
<Text style={styles.planTagText}>{plan.tag}</Text>
</View>
)}
<Text style={styles.planCardTitle}>{displayTitle}</Text>
</View>
<Text style={styles.planCardTitle}>{displayTitle}</Text>
<Text style={[styles.planCardPrice, styleConfig && { color: styleConfig.accent }]}>
{priceLabel || '--'}
</Text>
{plan.originalPrice && (
<Text style={styles.planCardOriginalPrice}>{plan.originalPrice}</Text>
)}
<Text style={styles.planCardDescription}>{plan.subtitle}</Text>
<View style={styles.planCardMiddleSection}>
<Text style={[styles.planCardPrice, styleConfig && { color: styleConfig.accent }]}>
{priceLabel || '--'}
</Text>
{plan.originalPrice && (
<Text style={styles.planCardOriginalPrice}>{plan.originalPrice}</Text>
)}
</View>
<View style={styles.planCardBottomSection}>
<Text style={styles.planCardDescription}>{plan.subtitle}</Text>
</View>
</LinearGradient>
</TouchableOpacity>
);
@@ -807,7 +946,9 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
showsVerticalScrollIndicator={false}
>
<View style={styles.sectionCard}>
<View style={[styles.sectionCard, {
marginTop: 56
}]}>
<View style={styles.sectionTitleRow}>
<View style={styles.sectionTitleBadge}>
<Ionicons name="star" size={16} color="#7B2CBF" />
@@ -860,20 +1001,27 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
index % 2 === 1 && styles.tableRowAlt,
]}
>
<Text style={[styles.tableCellText, styles.tableTitleCell]}>{row.title}</Text>
<View style={styles.tableVipCell}>
{row.vip ? (
<Ionicons name="checkmark-circle" size={20} color="#FFB200" />
) : (
<Ionicons name="remove" size={20} color="#D1D4DA" />
<View style={styles.tableTitleCell}>
<Text style={styles.tableCellText}>{row.title}</Text>
{row.description && (
<Text style={styles.tableDescriptionText}>{row.description}</Text>
)}
</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}>
{row.regular ? (
<Ionicons name="checkmark-circle" size={20} color="#8E8E93" />
) : (
<Ionicons name="remove" size={20} color="#D1D4DA" />
)}
<View style={styles.permissionContainer}>
{getPermissionIcon(row.regular.type, false)}
<Text style={styles.permissionText}>{row.regular.vipText || row.regular.text}</Text>
</View>
</View>
</View>
))}
@@ -881,38 +1029,6 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
</View>
<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}>
<CustomCheckBox
checked={agreementAccepted}
@@ -964,6 +1080,83 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
</TouchableOpacity>
</View>
</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>
</Modal>
@@ -991,7 +1184,7 @@ const styles = StyleSheet.create({
},
modalContentContainer: {
paddingHorizontal: 20,
paddingBottom: 32,
paddingBottom: 100, // 增加底部内边距,避免内容被悬浮按钮遮挡
paddingTop: 16,
},
floatingBackButton: {
@@ -1080,9 +1273,11 @@ const styles = StyleSheet.create({
elevation: 4,
},
planCardGradient: {
flex: 1,
paddingHorizontal: 16,
paddingVertical: 18,
minHeight: 170,
justifyContent: 'space-between',
},
planTag: {
alignSelf: 'flex-start',
@@ -1111,14 +1306,26 @@ const styles = StyleSheet.create({
fontSize: 13,
color: '#8E8EA1',
textDecorationLine: 'line-through',
marginTop: 4,
marginTop: 2,
},
planCardDescription: {
fontSize: 12,
color: '#6C6C77',
marginTop: 12,
lineHeight: 17,
},
planCardTopSection: {
flex: 1,
justifyContent: 'flex-start',
},
planCardMiddleSection: {
flex: 1,
justifyContent: 'center',
alignItems: 'flex-start',
},
planCardBottomSection: {
flex: 1,
justifyContent: 'flex-end',
},
tipsContainer: {
flexDirection: 'row',
alignItems: 'center',
@@ -1163,15 +1370,18 @@ const styles = StyleSheet.create({
color: '#3E3E44',
},
tableTitleCell: {
flex: 1.3,
flex: 1.5,
justifyContent: 'center',
},
tableVipCell: {
flex: 0.8,
alignItems: 'center',
justifyContent: 'center',
},
tableNormalCell: {
flex: 0.8,
alignItems: 'center',
justifyContent: 'center',
},
tableRowAlt: {
backgroundColor: '#FBFBFF',
@@ -1188,26 +1398,27 @@ const styles = StyleSheet.create({
shadowOffset: { width: 0, height: 6 },
elevation: 1,
},
noticeBanner: {
backgroundColor: '#FFE7E0',
borderRadius: 12,
paddingVertical: 10,
paddingHorizontal: 16,
marginBottom: 16,
},
noticeText: {
color: '#D35400',
fontSize: 13,
textAlign: 'center',
fontWeight: '600',
},
purchaseButton: {
backgroundColor: '#151515',
borderRadius: 28,
height: 52,
justifyContent: '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: {
backgroundColor: '#C6C6C8',
@@ -1228,23 +1439,23 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
flexWrap: 'nowrap',
marginBottom: 16,
},
agreementPrefix: {
fontSize: 11,
fontSize: 10,
color: '#666672',
marginHorizontal: 6,
marginRight: 4,
},
agreementLink: {
fontSize: 11,
fontSize: 10,
color: '#E91E63',
textDecorationLine: 'underline',
fontWeight: '500',
marginHorizontal: 4,
marginHorizontal: 2,
},
agreementSeparator: {
fontSize: 11,
fontSize: 10,
color: '#A0A0B0',
marginHorizontal: 2,
},
@@ -1271,4 +1482,24 @@ const styles = StyleSheet.create({
disabledPlanCard: {
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,
},
});