feat(membership): 重构会员权益对比表并优化购买界面布局
- 重构权益对比数据结构,支持独占、有限、无限三种权限类型 - 新增权限图标显示逻辑,区分VIP和普通用户权限状态 - 优化会员卡片布局,采用三段式布局提升视觉效果 - 实现悬浮购买按钮,支持Liquid Glass毛玻璃效果 - 增强购买流程验证,添加自动选择产品和详细错误处理 - 调整界面间距和样式,提升整体用户体验
This commit is contained in:
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user