feat(membership): 重构会员购买界面并添加图标库使用规范
- 将 MaterialIcons 替换为 Ionicons 以保持图标库一致性 - 重新设计会员购买界面,采用分段卡片布局和权益对比表格 - 添加 Liquid Glass 兼容的悬浮返回按钮 - 优化套餐卡片样式,使用渐变背景和标签展示 - 添加会员权益对比功能,清晰展示 VIP 与普通用户差异 - 更新任务文档,记录图标库使用规范和按钮组件 Liquid Glass 兼容性实现模式
This commit is contained in:
@@ -1,5 +1,141 @@
|
||||
# 常见任务和模式
|
||||
|
||||
## 图标库使用规范 - 禁止使用 MaterialIcons
|
||||
|
||||
**最后更新**: 2025-10-24
|
||||
|
||||
### 重要规则
|
||||
**项目中不允许使用 MaterialIcons**,所有图标必须使用 Ionicons 以保持图标库的一致性。
|
||||
|
||||
### 问题描述
|
||||
在项目中发现使用 MaterialIcons 的情况,需要将所有 MaterialIcons 替换为 Ionicons,以保持图标库的一致性。
|
||||
|
||||
### 解决方案
|
||||
将所有 MaterialIcons 导入和使用替换为对应的 Ionicons。
|
||||
|
||||
### 实现模式
|
||||
|
||||
#### 1. 替换导入语句
|
||||
```typescript
|
||||
// ❌ 禁止使用
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
|
||||
// ✅ 正确写法
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
```
|
||||
|
||||
#### 2. 替换图标名称和属性
|
||||
```typescript
|
||||
// ❌ 禁止使用
|
||||
<MaterialIcons name="arrow-back-ios" size={20} color="#333" />
|
||||
|
||||
// ✅ 正确写法 - 使用 HeaderBar 中的返回按钮实现
|
||||
<Ionicons name="chevron-back" size={24} color="#333" />
|
||||
```
|
||||
|
||||
#### 3. 常见图标映射
|
||||
- `arrow-back-ios` → `chevron-back` (返回按钮)
|
||||
- `auto-awesome` → `star` (星星/自动推荐)
|
||||
- `tips-and-updates` → `bulb` (提示/建议)
|
||||
- `fact-check` → `checkbox` (检查/确认)
|
||||
- `check-circle` → `checkmark-circle` (勾选圆圈)
|
||||
- `remove` → `remove` (移除/删除,名称相同)
|
||||
|
||||
### 重要注意事项
|
||||
1. **图标大小调整**:Ionicons 和 MaterialIcons 的默认大小可能不同,需要适当调整
|
||||
2. **图标名称差异**:两个图标库的图标名称不同,需要找到对应的功能图标
|
||||
3. **样式一致性**:确保替换后的图标在视觉上与原设计保持一致
|
||||
4. **Liquid Glass 兼容性**:替换后的图标需要继续支持 Liquid Glass 效果
|
||||
5. **代码审查**:在代码审查中需要特别检查是否使用了 MaterialIcons
|
||||
|
||||
### 参考实现
|
||||
- `components/ui/HeaderBar.tsx` - 返回按钮的标准实现
|
||||
- `components/model/MembershipModal.tsx` - 完整的 MaterialIcons 替换示例
|
||||
|
||||
## 按钮组件 Liquid Glass 兼容性
|
||||
|
||||
**最后更新**: 2025-10-24
|
||||
|
||||
### 重要原则
|
||||
**所有按钮组件都需要尝试兼容 Liquid Glass**,这是项目的设计要求。
|
||||
|
||||
### 实现模式
|
||||
|
||||
#### 1. 导入必要的组件
|
||||
```typescript
|
||||
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||
```
|
||||
|
||||
#### 2. 检查设备支持情况
|
||||
```typescript
|
||||
const isGlassAvailable = isLiquidGlassAvailable();
|
||||
```
|
||||
|
||||
#### 3. 实现条件渲染的按钮
|
||||
```typescript
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
disabled={isLoading}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={styles.button}
|
||||
glassEffectStyle="clear" // 或 "regular"
|
||||
tintColor="rgba(255, 255, 255, 0.3)" // 自定义色调
|
||||
isInteractive={true} // 启用交互反馈
|
||||
>
|
||||
<Ionicons name="icon-name" size={20} color="#333" />
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.button, styles.fallbackButton]}>
|
||||
<Ionicons name="icon-name" size={20} color="#333" />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
```
|
||||
|
||||
#### 4. 定义样式
|
||||
```typescript
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20, // 圆形按钮
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden', // 保证玻璃边界圆角效果
|
||||
// 其他通用样式...
|
||||
},
|
||||
fallbackButton: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 重要注意事项
|
||||
1. **兼容性检查**:必须使用 `isLiquidGlassAvailable()` 检查设备支持情况
|
||||
2. **overflow: 'hidden'**:GlassView 组件需要设置此属性以保证圆角效果
|
||||
3. **降级样式**:为不支持 Liquid Glass 的设备提供视觉上相似的替代方案
|
||||
4. **交互反馈**:设置 `isInteractive={true}` 启用原生的触觉反馈
|
||||
5. **图标居中**:确保使用 `alignItems: 'center'` 和 `justifyContent: 'center'` 使图标完全居中
|
||||
6. **色调自定义**:通过 `tintColor` 属性自定义按钮的颜色主题
|
||||
|
||||
### 常用配置
|
||||
- **glassEffectStyle**: "clear"(透明)或 "regular"(常规)
|
||||
- **tintColor**: 根据按钮功能选择合适的颜色
|
||||
- 返回/导航操作:白色系 `rgba(255, 255, 255, 0.3)`
|
||||
- 删除操作:红色系 `rgba(244, 67, 54, 0.2)`
|
||||
- 确认操作:绿色系 `rgba(76, 175, 80, 0.2)`
|
||||
- 信息操作:蓝色系 `rgba(33, 150, 243, 0.2)`
|
||||
|
||||
### 参考实现
|
||||
- `components/model/MembershipModal.tsx` - 悬浮返回按钮
|
||||
- `components/glass/button.tsx` - 通用 Glass 按钮组件
|
||||
- `app/(tabs)/_layout.tsx` - 标签栏按钮实现
|
||||
|
||||
## HeaderBar 顶部距离处理
|
||||
|
||||
**最后更新**: 2025-10-16
|
||||
|
||||
@@ -11,8 +11,10 @@ import {
|
||||
captureUserAction
|
||||
} from '@/utils/sentry.utils';
|
||||
import { Toast as GlobalToast } from '@/utils/toast.utils';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { captureException } from '@sentry/react-native';
|
||||
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -42,6 +44,8 @@ interface MembershipPlan {
|
||||
subtitle: string;
|
||||
type: 'weekly' | 'quarterly' | 'lifetime';
|
||||
recommended?: boolean;
|
||||
tag?: string;
|
||||
originalPrice?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_PLANS: MembershipPlan[] = [
|
||||
@@ -51,21 +55,50 @@ const DEFAULT_PLANS: MembershipPlan[] = [
|
||||
subtitle: '一次投入,终身健康陪伴',
|
||||
type: 'lifetime',
|
||||
recommended: true,
|
||||
tag: '限时特价',
|
||||
originalPrice: '¥898',
|
||||
},
|
||||
{
|
||||
id: 'com.anonymous.digitalpilates.membership.quarter',
|
||||
fallbackTitle: '季度会员',
|
||||
subtitle: '3个月蜕变计划,见证身材变化',
|
||||
type: 'quarterly',
|
||||
originalPrice: '¥598',
|
||||
},
|
||||
{
|
||||
id: 'com.anonymous.digitalpilates.membership.weekly',
|
||||
fallbackTitle: '周会员',
|
||||
subtitle: '7天体验,开启健康第一步',
|
||||
type: 'weekly',
|
||||
originalPrice: '¥128',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
const PLAN_STYLE_CONFIG: Record<MembershipPlan['type'], { gradient: readonly [string, string]; accent: string }> = {
|
||||
lifetime: {
|
||||
gradient: ['#FFF1DD', '#FFE8FA'] as const,
|
||||
accent: '#7B2CBF',
|
||||
},
|
||||
quarterly: {
|
||||
gradient: ['#E5EAFE', '#F4E8FF'] as const,
|
||||
accent: '#5B5BE6',
|
||||
},
|
||||
weekly: {
|
||||
gradient: ['#FFF6E5', '#FFE7CC'] as const,
|
||||
accent: '#FF9500',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export function MembershipModal({ visible, onClose, onPurchaseSuccess }: MembershipModalProps) {
|
||||
const [selectedProduct, setSelectedProduct] = useState<PurchasesStoreProduct | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -672,38 +705,6 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
}
|
||||
};
|
||||
|
||||
const renderMembershipBenefits = () => (
|
||||
<View style={styles.benefitsContainer}>
|
||||
<View style={styles.benefitsTitleContainer}>
|
||||
<Text style={styles.benefitsTitle}>解锁全部健康功能</Text>
|
||||
<Text style={styles.benefitsSubtitle}>开启您的健康蜕变之旅</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.benefitItem}>
|
||||
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
|
||||
<Text style={styles.benefitText}>高级营养分析,精准卡路里计算</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.benefitItem}>
|
||||
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
|
||||
<Text style={styles.benefitText}>定制化减脂计划,科学体重管理</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.benefitItem}>
|
||||
<MaterialIcons name="check-circle" size={20} color="#DF42D0" />
|
||||
<Text style={styles.benefitText}>深度健康数据分析,洞察身体变化</Text>
|
||||
</View>
|
||||
|
||||
|
||||
{/* 皇冠图标 */}
|
||||
<View style={styles.crownContainer}>
|
||||
{/* <Image
|
||||
source={require('@/assets/images/img_profile_vip_bg.png')}
|
||||
style={styles.crownIcon}
|
||||
/> */}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderPlanCard = (product: PurchasesStoreProduct) => {
|
||||
const plan = DEFAULT_PLANS.find(p => p.id === product.identifier);
|
||||
@@ -715,13 +716,14 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
const isSelected = selectedProduct === product;
|
||||
const displayTitle = product.title || plan.fallbackTitle;
|
||||
const priceLabel = product.priceString || '';
|
||||
const styleConfig = PLAN_STYLE_CONFIG[plan.type];
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={product.identifier}
|
||||
style={[
|
||||
styles.planCard,
|
||||
isSelected && styles.selectedPlan,
|
||||
styles.planCardWrapper,
|
||||
isSelected && styles.planCardWrapperSelected,
|
||||
loading && styles.disabledPlanCard,
|
||||
]}
|
||||
onPress={() => !loading && product && setSelectedProduct(product)}
|
||||
@@ -732,35 +734,27 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
accessibilityHint={loading ? '购买进行中,无法切换套餐' : `选择${displayTitle}套餐`}
|
||||
accessibilityState={{ disabled: loading, selected: isSelected }}
|
||||
>
|
||||
{plan.recommended && (
|
||||
<View style={styles.recommendedBadge}>
|
||||
<Text style={styles.recommendedText}>推荐</Text>
|
||||
</View>
|
||||
)}
|
||||
<LinearGradient
|
||||
colors={styleConfig?.gradient ?? ['#FFFFFF', '#FFFFFF']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
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.planHeader}>
|
||||
<Text style={[
|
||||
styles.planTitle,
|
||||
plan.type === 'lifetime' && styles.lifetimePlanTitle,
|
||||
plan.type === 'quarterly' && styles.quarterlyPlanTitle,
|
||||
plan.type === 'weekly' && styles.weeklyPlanTitle,
|
||||
]}>
|
||||
{displayTitle}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.planPricing}>
|
||||
<Text style={[
|
||||
styles.planPrice,
|
||||
plan.type === 'lifetime' && styles.lifetimePlanPrice,
|
||||
plan.type === 'quarterly' && styles.quarterlyPlanPrice,
|
||||
plan.type === 'weekly' && styles.weeklyPlanPrice,
|
||||
]}>
|
||||
<Text style={styles.planCardTitle}>{displayTitle}</Text>
|
||||
<Text style={[styles.planCardPrice, styleConfig && { color: styleConfig.accent }]}>
|
||||
{priceLabel || '--'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.planSubtitle}>{plan.subtitle}</Text>
|
||||
{plan.originalPrice && (
|
||||
<Text style={styles.planCardOriginalPrice}>{plan.originalPrice}</Text>
|
||||
)}
|
||||
<Text style={styles.planCardDescription}>{plan.subtitle}</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -770,7 +764,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent={true}
|
||||
animationType="slide"
|
||||
animationType="fade"
|
||||
presentationStyle="overFullScreen"
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
@@ -781,119 +775,194 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onPress={onClose} // 阻止点击背景关闭
|
||||
/>
|
||||
|
||||
{/* 会员内容 */}
|
||||
<View style={styles.modalContainer}>
|
||||
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
|
||||
{/* 关闭按钮 */}
|
||||
{/* <TouchableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={onClose}
|
||||
>
|
||||
<Text style={styles.closeButtonText}>×</Text>
|
||||
</TouchableOpacity> */}
|
||||
|
||||
|
||||
{/* 会员权益介绍 */}
|
||||
{renderMembershipBenefits()}
|
||||
|
||||
{/* 会员套餐选择 */}
|
||||
{products.length === 0 && (
|
||||
<View style={styles.configurationNotice}>
|
||||
<Text style={styles.configurationText}>
|
||||
暂未获取到会员商品,请在 RevenueCat 中配置 iOS 产品并同步到当前 Offering。
|
||||
</Text>
|
||||
{/* 悬浮返回按钮 - 移到 ScrollView 外部以确保始终在最上层 */}
|
||||
<TouchableOpacity
|
||||
onPress={onClose}
|
||||
activeOpacity={0.7}
|
||||
accessible={true}
|
||||
accessibilityLabel="返回"
|
||||
accessibilityHint="关闭会员购买弹窗"
|
||||
style={styles.floatingBackButtonContainer}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={styles.floatingBackButton}
|
||||
glassEffectStyle="clear"
|
||||
tintColor="rgba(255, 255, 255, 0.3)"
|
||||
isInteractive={true}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color="#333" />
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.floatingBackButton, styles.fallbackBackButton]}>
|
||||
<Ionicons name="chevron-back" size={24} color="#333" />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.plansContainer}>
|
||||
{products.map(renderPlanCard)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 产品选择提示区域 */}
|
||||
{selectedProduct && (
|
||||
<View style={styles.tipsContainer}>
|
||||
<Text style={styles.tipsText}>
|
||||
{getTipsContent(selectedProduct)}
|
||||
</Text>
|
||||
<ScrollView
|
||||
style={styles.modalContent}
|
||||
contentContainerStyle={styles.modalContentContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
|
||||
<View style={styles.sectionCard}>
|
||||
<View style={styles.sectionTitleRow}>
|
||||
<View style={styles.sectionTitleBadge}>
|
||||
<Ionicons name="star" size={16} color="#7B2CBF" />
|
||||
</View>
|
||||
<Text style={styles.sectionTitle}>会员套餐</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.sectionSubtitle}>灵活选择,跟随节奏稳步提升</Text>
|
||||
|
||||
{/* 协议同意区域 */}
|
||||
<View style={styles.agreementRow}>
|
||||
<CustomCheckBox
|
||||
checked={agreementAccepted}
|
||||
onCheckedChange={setAgreementAccepted}
|
||||
size={16}
|
||||
checkedColor="#E91E63"
|
||||
uncheckedColor="#999"
|
||||
/>
|
||||
<Text style={styles.agreementPrefix}>我已阅读并同意</Text>
|
||||
<TouchableOpacity onPress={() => {
|
||||
Linking.openURL(USER_AGREEMENT_URL);
|
||||
captureMessage('click user agreement');
|
||||
}}>
|
||||
<Text style={styles.agreementLink}>《用户协议》</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.agreementSeparator}> | </Text>
|
||||
<TouchableOpacity onPress={() => {
|
||||
// Linking.openURL(MEMBERSHIP_AGREEMENT_URL);
|
||||
captureMessage('click membership agreement');
|
||||
}}>
|
||||
<Text style={styles.agreementLink}>《会员协议》</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.agreementSeparator}> | </Text>
|
||||
<TouchableOpacity onPress={() => {
|
||||
// Linking.openURL(AUTO_RENEWAL_AGREEMENT_URL);
|
||||
captureMessage('click auto renewal agreement');
|
||||
}}>
|
||||
<Text style={styles.agreementLink}>《自动续费协议》</Text>
|
||||
</TouchableOpacity>
|
||||
{products.length === 0 ? (
|
||||
<View style={styles.configurationNotice}>
|
||||
<Text style={styles.configurationText}>
|
||||
暂未获取到会员商品,请在 RevenueCat 中配置 iOS 产品并同步到当前 Offering。
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View style={styles.plansContainer}>
|
||||
{products.map(renderPlanCard)}
|
||||
</View>
|
||||
{selectedProduct && (
|
||||
<View style={styles.tipsContainer}>
|
||||
<Ionicons name="bulb" size={18} color="#F29F05" />
|
||||
<Text style={styles.tipsText}>{getTipsContent(selectedProduct)}</Text>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
||||
{/* 购买按钮 */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.purchaseButton,
|
||||
(loading || !selectedProduct) && styles.disabledButton
|
||||
]}
|
||||
onPress={handlePurchase}
|
||||
disabled={loading || !selectedProduct}
|
||||
accessible={true}
|
||||
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
|
||||
accessibilityHint={
|
||||
loading
|
||||
? '购买正在进行中,请稍候'
|
||||
: selectedProduct
|
||||
? `点击购买${selectedProduct.title || '已选'}会员套餐`
|
||||
: '请选择会员套餐后再进行购买'
|
||||
}
|
||||
accessibilityState={{ disabled: loading || !selectedProduct }}
|
||||
>
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
|
||||
<Text style={styles.purchaseButtonText}>正在处理购买...</Text>
|
||||
<View style={styles.sectionCard}>
|
||||
<View style={styles.sectionTitleRow}>
|
||||
<View style={styles.sectionTitleBadge}>
|
||||
<Ionicons name="checkbox" size={16} color="#FF9F0A" />
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.purchaseButtonText}>开启健康蜕变</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.sectionTitle}>权益对比</Text>
|
||||
</View>
|
||||
<Text style={styles.sectionSubtitle}>核心权益一目了然,选择更安心</Text>
|
||||
|
||||
{/* 恢复购买按钮 */}
|
||||
<TouchableOpacity
|
||||
style={[styles.restoreButton, (restoring || loading) && styles.disabledRestoreButton]}
|
||||
onPress={handleRestore}
|
||||
disabled={restoring || loading}
|
||||
>
|
||||
{restoring ? (
|
||||
<View style={styles.restoreButtonContent}>
|
||||
<ActivityIndicator size="small" color="#666" style={styles.restoreButtonLoader} />
|
||||
<Text style={styles.restoreButtonText}>恢复中...</Text>
|
||||
<View style={styles.comparisonTable}>
|
||||
<View style={[styles.tableRow, styles.tableHeader]}>
|
||||
<Text style={[styles.tableHeaderText, styles.tableTitleCell]}>权益</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableVipCell]}>VIP</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableNormalCell]}>普通用户</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.restoreButtonText}>恢复购买</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{BENEFIT_COMPARISON.map((row, index) => (
|
||||
<View
|
||||
key={row.title}
|
||||
style={[
|
||||
styles.tableRow,
|
||||
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>
|
||||
<View style={styles.tableNormalCell}>
|
||||
{row.regular ? (
|
||||
<Ionicons name="checkmark-circle" size={20} color="#8E8E93" />
|
||||
) : (
|
||||
<Ionicons name="remove" size={20} color="#D1D4DA" />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</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}
|
||||
onCheckedChange={setAgreementAccepted}
|
||||
size={16}
|
||||
checkedColor="#E91E63"
|
||||
uncheckedColor="#999"
|
||||
/>
|
||||
<Text style={styles.agreementPrefix}>开通即视为同意</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL(USER_AGREEMENT_URL);
|
||||
captureMessage('click user agreement');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.agreementLink}>《用户协议》</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.agreementSeparator}>|</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
captureMessage('click membership agreement');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.agreementLink}>《会员协议》</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.agreementSeparator}>|</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
captureMessage('click auto renewal agreement');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.agreementLink}>《自动续费协议》</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.restoreButton, (restoring || loading) && styles.disabledRestoreButton]}
|
||||
onPress={handleRestore}
|
||||
disabled={restoring || loading}
|
||||
>
|
||||
{restoring ? (
|
||||
<View style={styles.restoreButtonContent}>
|
||||
<ActivityIndicator size="small" color="#666" style={styles.restoreButtonLoader} />
|
||||
<Text style={styles.restoreButtonText}>恢复中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.restoreButtonText}>恢复购买</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
@@ -908,231 +977,284 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
backdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.35)',
|
||||
},
|
||||
modalContainer: {
|
||||
height: height * 0.6,
|
||||
backgroundColor: 'white',
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
height: height * 0.92,
|
||||
backgroundColor: '#F5F6FA',
|
||||
borderTopLeftRadius: 28,
|
||||
borderTopRightRadius: 28,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
modalContent: {
|
||||
flex: 1,
|
||||
},
|
||||
modalContentContainer: {
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 32,
|
||||
paddingTop: 16,
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||
justifyContent: 'center',
|
||||
floatingBackButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
alignItems: 'center',
|
||||
zIndex: 1,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
closeButtonText: {
|
||||
floatingBackButtonContainer: {
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
left: 20,
|
||||
zIndex: 10, // 确保按钮在最上层
|
||||
},
|
||||
fallbackBackButton: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
|
||||
},
|
||||
sectionCard: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 18,
|
||||
marginBottom: 20,
|
||||
shadowColor: '#1C1C1E',
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 12,
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
elevation: 2,
|
||||
},
|
||||
sectionTitleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
sectionTitleBadge: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#F4EFFD',
|
||||
marginRight: 8,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
color: '#666',
|
||||
fontWeight: '300',
|
||||
fontWeight: '700',
|
||||
color: '#2B2B2E',
|
||||
},
|
||||
sectionSubtitle: {
|
||||
fontSize: 13,
|
||||
color: '#6B6B73',
|
||||
marginTop: 6,
|
||||
marginBottom: 16,
|
||||
},
|
||||
configurationNotice: {
|
||||
backgroundColor: '#FFF9E6',
|
||||
borderRadius: 12,
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
marginTop: 30,
|
||||
marginBottom: 20,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#FFE4B5',
|
||||
backgroundColor: '#FFF4E5',
|
||||
},
|
||||
configurationText: {
|
||||
fontSize: 14,
|
||||
color: '#B8860B',
|
||||
color: '#B86A04',
|
||||
textAlign: 'center',
|
||||
marginTop: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
configurationButton: {
|
||||
backgroundColor: '#FF9500',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
configurationButtonText: {
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
benefitsContainer: {
|
||||
position: 'relative',
|
||||
backgroundColor: '#FFE7F9',
|
||||
borderRadius: 16,
|
||||
marginBottom: 30,
|
||||
padding: 20,
|
||||
},
|
||||
benefitsTitleContainer: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
benefitsTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
marginBottom: 5,
|
||||
},
|
||||
benefitsSubtitle: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
textAlign: 'center',
|
||||
},
|
||||
benefitsTitleBg: {
|
||||
width: 180,
|
||||
height: 20,
|
||||
marginBottom: 10,
|
||||
},
|
||||
benefitItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
benefitText: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
marginLeft: 8,
|
||||
fontWeight: '500',
|
||||
},
|
||||
crownContainer: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 10,
|
||||
width: 100,
|
||||
height: 100,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
crownIcon: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
resizeMode: 'contain',
|
||||
lineHeight: 20,
|
||||
},
|
||||
plansContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 20,
|
||||
},
|
||||
planCard: {
|
||||
planCardWrapper: {
|
||||
flex: 1,
|
||||
marginHorizontal: 4,
|
||||
borderRadius: 12,
|
||||
borderWidth: 2,
|
||||
borderColor: '#E0E0E0',
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: 12,
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 18,
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(123,44,191,0.08)',
|
||||
},
|
||||
selectedPlan: {
|
||||
borderColor: '#DF42D0',
|
||||
backgroundColor: '#FFF5FE',
|
||||
planCardWrapperSelected: {
|
||||
borderColor: '#7B2CBF',
|
||||
shadowColor: '#7B2CBF',
|
||||
shadowOpacity: 0.18,
|
||||
shadowRadius: 16,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
elevation: 4,
|
||||
},
|
||||
recommendedBadge: {
|
||||
position: 'absolute',
|
||||
top: -10,
|
||||
backgroundColor: '#7B2CBF',
|
||||
borderRadius: 10,
|
||||
planCardGradient: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 18,
|
||||
minHeight: 170,
|
||||
},
|
||||
planTag: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: '#2F2F36',
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
marginBottom: 12,
|
||||
},
|
||||
recommendedText: {
|
||||
color: 'white',
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
planHeader: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
planTitle: {
|
||||
fontSize: 16,
|
||||
planTagText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
},
|
||||
lifetimePlanTitle: {
|
||||
color: '#7B2CBF',
|
||||
planCardTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#241F1F',
|
||||
},
|
||||
quarterlyPlanTitle: {
|
||||
color: '#DF42D0',
|
||||
planCardPrice: {
|
||||
fontSize: 28,
|
||||
fontWeight: '700',
|
||||
marginTop: 12,
|
||||
},
|
||||
weeklyPlanTitle: {
|
||||
color: '#FF9500',
|
||||
planCardOriginalPrice: {
|
||||
fontSize: 13,
|
||||
color: '#8E8EA1',
|
||||
textDecorationLine: 'line-through',
|
||||
marginTop: 4,
|
||||
},
|
||||
planPricing: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
planPrice: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
},
|
||||
lifetimePlanPrice: {
|
||||
color: '#7B2CBF',
|
||||
},
|
||||
quarterlyPlanPrice: {
|
||||
color: '#DF42D0',
|
||||
},
|
||||
weeklyPlanPrice: {
|
||||
color: '#FF9500',
|
||||
},
|
||||
planSubtitle: {
|
||||
planCardDescription: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
textAlign: 'center',
|
||||
color: '#6C6C77',
|
||||
marginTop: 12,
|
||||
lineHeight: 17,
|
||||
},
|
||||
tipsContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FEF4E6',
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
marginTop: 16,
|
||||
},
|
||||
tipsText: {
|
||||
flex: 1,
|
||||
fontSize: 12,
|
||||
color: '#9B6200',
|
||||
marginLeft: 6,
|
||||
lineHeight: 16,
|
||||
},
|
||||
purchaseButton: {
|
||||
backgroundColor: '#DF42D0',
|
||||
borderRadius: 25,
|
||||
height: 50,
|
||||
justifyContent: 'center',
|
||||
comparisonTable: {
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderColor: '#ECECF3',
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 15,
|
||||
marginTop: 10,
|
||||
shadowColor: '#DF42D0',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 6,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
disabledButton: {
|
||||
backgroundColor: '#ccc',
|
||||
shadowOpacity: 0,
|
||||
elevation: 0,
|
||||
tableHeader: {
|
||||
backgroundColor: '#F8F8FF',
|
||||
},
|
||||
purchaseButtonText: {
|
||||
color: 'white',
|
||||
fontSize: 18,
|
||||
tableHeaderText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#575764',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.4,
|
||||
},
|
||||
tableCellText: {
|
||||
fontSize: 13,
|
||||
color: '#3E3E44',
|
||||
},
|
||||
tableTitleCell: {
|
||||
flex: 1.3,
|
||||
},
|
||||
tableVipCell: {
|
||||
flex: 0.8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
tableNormalCell: {
|
||||
flex: 0.8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
tableRowAlt: {
|
||||
backgroundColor: '#FBFBFF',
|
||||
},
|
||||
bottomSection: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 20,
|
||||
marginBottom: 10,
|
||||
shadowColor: '#1C1C1E',
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 12,
|
||||
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',
|
||||
},
|
||||
restoreButton: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 25,
|
||||
height: 50,
|
||||
purchaseButton: {
|
||||
backgroundColor: '#151515',
|
||||
borderRadius: 28,
|
||||
height: 52,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
marginBottom: 16,
|
||||
},
|
||||
disabledButton: {
|
||||
backgroundColor: '#C6C6C8',
|
||||
},
|
||||
purchaseButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
loadingContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingSpinner: {
|
||||
marginRight: 8,
|
||||
},
|
||||
agreementRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
marginBottom: 16,
|
||||
},
|
||||
agreementPrefix: {
|
||||
fontSize: 11,
|
||||
color: '#666672',
|
||||
marginHorizontal: 6,
|
||||
},
|
||||
agreementLink: {
|
||||
fontSize: 11,
|
||||
color: '#E91E63',
|
||||
textDecorationLine: 'underline',
|
||||
fontWeight: '500',
|
||||
marginHorizontal: 4,
|
||||
},
|
||||
agreementSeparator: {
|
||||
fontSize: 11,
|
||||
color: '#A0A0B0',
|
||||
marginHorizontal: 2,
|
||||
},
|
||||
restoreButton: {
|
||||
alignSelf: 'center',
|
||||
paddingVertical: 6,
|
||||
},
|
||||
restoreButtonText: {
|
||||
color: '#666',
|
||||
fontSize: 16,
|
||||
color: '#6F6F7A',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
disabledRestoreButton: {
|
||||
@@ -1146,49 +1268,7 @@ const styles = StyleSheet.create({
|
||||
restoreButtonLoader: {
|
||||
marginRight: 8,
|
||||
},
|
||||
loadingContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
loadingSpinner: {
|
||||
marginRight: 8,
|
||||
},
|
||||
disabledPlanCard: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
agreementRow: {
|
||||
width: '100%',
|
||||
marginTop: 6,
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
agreementPrefix: {
|
||||
fontSize: 10,
|
||||
color: '#333',
|
||||
marginRight: 4,
|
||||
},
|
||||
agreementLink: {
|
||||
fontSize: 10,
|
||||
color: '#E91E63',
|
||||
textDecorationLine: 'underline',
|
||||
fontWeight: '500',
|
||||
},
|
||||
agreementSeparator: {
|
||||
fontSize: 10,
|
||||
color: '#666',
|
||||
marginHorizontal: 2,
|
||||
},
|
||||
tipsContainer: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
tipsText: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
lineHeight: 18,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user