This commit is contained in:
imeepos
2026-01-28 15:57:40 +08:00
parent 7d73cfbc3e
commit efd4aba8c1
12 changed files with 676 additions and 262 deletions

View File

@@ -59,14 +59,27 @@ interface Plan {
price: number
recommended?: boolean
points: number // 每月积分
features: string[] // 功能列表
currency: string // 货币类型
featureList: string[] // 功能列表(翻译 key
}
// 货币符号转换函数
function toUnit(currency: string): string {
switch (currency.toLowerCase()) {
case 'hkd':
return 'HK$'
case 'cny':
return '¥'
default:
return '$'
}
}
export default function MembershipScreen() {
const router = useRouter()
const insets = useSafeAreaInsets()
const { width: screenWidth } = useWindowDimensions()
const [agreed, setAgreed] = useState(false)
const [agreed, setAgreed] = useState(true)
const [pointsDrawerVisible, setPointsDrawerVisible] = useState(false)
const [isSubscribing, setIsSubscribing] = useState(false)
@@ -89,38 +102,26 @@ export default function MembershipScreen() {
stripePricingData,
} = useMembership()
// 映射 API 数据到 UI 格式
const plans: Plan[] = creditPlans.map((plan, index) => ({
id: `plan-${index}` as PlanType,
name: index === 0 ? 'Plus' : index === 1 ? 'Pro' : 'Plus',
name: plan.name,
price: plan.amountInCents / 100,
recommended: plan.popular,
points: plan.credits,
features: [
t('membership.features.points750'),
t('membership.features.textToVideo'),
t('membership.features.superClearImage'),
t('membership.features.superDiscount'),
...(index > 0 ? [
t('membership.features.sora2ProTemplate'),
t('membership.features.removeWatermark'),
t('membership.features.allTemplates'),
] : []),
...(index > 1 ? [
t('membership.features.higherQuality'),
t('membership.features.prioritySupport'),
] : []),
],
currency: plan.currency,
featureList: plan.featureList,
}))
const selectedPlan = plans[selectedPlanIndex] || plans[0]
// 下拉菜单选项
const menuOptions = [
{ label: t('membership.terms'), value: 'terms' },
{ label: t('membership.privacy'), value: 'privacy' },
]
// 处理菜单选择
const handleMenuSelect = (value: string) => {
if (value === 'terms') {
@@ -129,7 +130,7 @@ export default function MembershipScreen() {
router.push('/privacy')
}
}
// 轮播图相关
const carouselImages = [
require('@/assets/images/membership.png'),
@@ -203,37 +204,37 @@ export default function MembershipScreen() {
</Pressable>
<View style={styles.headerSpacer} />
<View style={styles.pointsContainer}>
<Pressable
style={styles.pointsPill}
onPress={() => setPointsDrawerVisible(true)}
>
<Text style={styles.pointsLabel}>{t('membership.myPoints')}</Text>
<Pressable
style={styles.pointsPill}
onPress={() => setPointsDrawerVisible(true)}
>
<Text style={styles.pointsLabel}>{t('membership.myPoints')}</Text>
<MembershipPointsIcon />
<Text style={styles.pointsValue}>{creditBalance}</Text>
</Pressable>
<View style={styles.settingsButtonContainer}>
<Dropdown
options={menuOptions}
onSelect={(value) => handleMenuSelect(value)}
offsetTop={10}
renderTrigger={(selectedOption, isOpen, toggle) => (
<Pressable style={styles.settingsButton} onPress={toggle}>
<OmitIcon />
</Pressable>
)}
dropdownStyle={styles.menuDropdown}
renderOption={(option, isSelected) => (
<View style={styles.menuOption}>
{option.value === 'terms' ? (
<TermsIcon />
) : (
<PrivacyIcon />
)}
<Text style={styles.menuOptionText}>{option.label}</Text>
</View>
)}
/>
</View>
<Text style={styles.pointsValue}>{creditBalance}</Text>
</Pressable>
<View style={styles.settingsButtonContainer}>
<Dropdown
options={menuOptions}
onSelect={(value) => handleMenuSelect(value)}
offsetTop={10}
renderTrigger={(selectedOption, isOpen, toggle) => (
<Pressable style={styles.settingsButton} onPress={toggle}>
<OmitIcon />
</Pressable>
)}
dropdownStyle={styles.menuDropdown}
renderOption={(option, isSelected) => (
<View style={styles.menuOption}>
{option.value === 'terms' ? (
<TermsIcon />
) : (
<PrivacyIcon />
)}
<Text style={styles.menuOptionText}>{option.label}</Text>
</View>
)}
/>
</View>
</View>
</View>
<ScrollView
@@ -241,46 +242,46 @@ export default function MembershipScreen() {
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.imageContainer}>
<Carousel
width={screenWidth}
height={screenWidth / 1.1}
data={carouselImages}
renderItem={({ item }) => (
<Image
source={item}
style={[styles.membershipImage, { width: screenWidth, height: screenWidth / 1.1 }]}
contentFit="cover"
/>
)}
autoPlay
autoPlayInterval={2000}
loop
onSnapToItem={(index) => setCurrentImageIndex(index)}
enabled={false}
windowSize={1}
mode="parallax"
/>
<View style={styles.dotsContainer}>
{carouselImages.map((_, index) => (
<View
key={index}
style={[
styles.dot,
index === currentImageIndex && styles.dotActive,
]}
/>
))}
</View>
<LinearGradient
colors={['#090A0B00', '#090A0B']}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
style={styles.imageGradient}
pointerEvents="none"
/>
<View style={styles.imageContainer}>
<Carousel
width={screenWidth}
height={screenWidth / 1.1}
data={carouselImages}
renderItem={({ item }) => (
<Image
source={item}
style={[styles.membershipImage, { width: screenWidth, height: screenWidth / 1.1 }]}
contentFit="cover"
/>
)}
autoPlay
autoPlayInterval={2000}
loop
onSnapToItem={(index) => setCurrentImageIndex(index)}
enabled={false}
windowSize={1}
mode="parallax"
/>
<View style={styles.dotsContainer}>
{carouselImages.map((_, index) => (
<View
key={index}
style={[
styles.dot,
index === currentImageIndex && styles.dotActive,
]}
/>
))}
</View>
<LinearGradient
colors={['#090A0B00', '#090A0B']}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
style={styles.imageGradient}
pointerEvents="none"
/>
</View>
{/* 订阅计划标题 */}
<Text style={styles.sectionTitle}>{t('membership.subscriptionPlan')}</Text>
@@ -289,121 +290,128 @@ export default function MembershipScreen() {
{plans.map((plan, index) => {
const isSelected = selectedPlanIndex === index
return (
<Pressable
key={plan.id}
style={[
<Pressable
key={plan.id}
style={[
styles.planCardWrapper,
index > 0 && styles.planCardSpacing,
]}
onPress={() => setSelectedPlanIndex(index)}
>
{isSelected ? (
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.planCardGradient}
>
<View style={styles.planCard}>
{plan.recommended && (
<View style={styles.recommendedBadge}>
<Text style={styles.recommendedText}>{t('membership.mostRecommended')}</Text>
index > 0 && styles.planCardSpacing,
]}
onPress={() => setSelectedPlanIndex(index)}
>
{isSelected ? (
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.planCardGradient}
>
<View style={styles.planCard}>
{plan.recommended && (
<View style={styles.recommendedBadge}>
<Text style={styles.recommendedText}>{t('membership.mostRecommended')}</Text>
</View>
)}
<Text style={styles.planName}>{plan.name}</Text>
<View style={styles.priceContainer}>
<GradientText
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.priceSymbol}
>
{toUnit(plan.currency)}
</GradientText>
<GradientText
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.priceValue}>{plan.price}</GradientText>
<Text style={styles.priceUnit}>{t('membership.perMonth')}</Text>
</View>
</View>
</LinearGradient>
) : (
<View style={styles.planCardWrapperUnselected}>
<View style={styles.planCard}>
{plan.recommended && (
<View style={styles.recommendedBadge}>
<Text style={styles.recommendedText}>{t('membership.mostRecommended')}</Text>
</View>
)}
<Text style={styles.planName}>{plan.name}</Text>
<View style={styles.priceContainer}>
<GradientText
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.priceSymbol}
>
{toUnit(plan.currency)}
</GradientText>
<GradientText
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.priceValue}>{plan.price}</GradientText>
<Text style={styles.priceUnit}>{t('membership.perMonth')}</Text>
</View>
)}
<Text style={styles.planName}>{plan.name}</Text>
<View style={styles.priceContainer}>
<GradientText
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.priceSymbol}
>
$
</GradientText>
<GradientText
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.priceValue}>{plan.price}</GradientText>
<Text style={styles.priceUnit}>{t('membership.perMonth')}</Text>
</View>
</View>
</LinearGradient>
) : (
<View style={styles.planCardWrapperUnselected}>
<View style={styles.planCard}>
{plan.recommended && (
<View style={styles.recommendedBadge}>
<Text style={styles.recommendedText}>{t('membership.mostRecommended')}</Text>
</View>
)}
<Text style={styles.planName}>{plan.name}</Text>
<View style={styles.priceContainer}>
<GradientText
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.priceSymbol}
>
$
</GradientText>
<GradientText
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.priceValue}>{plan.price}</GradientText>
<Text style={styles.priceUnit}>{t('membership.perMonth')}</Text>
</View>
</View>
</View>
)}
</Pressable>
)}
</Pressable>
)
})}
</View>
{/* 卡片所属的信息 */}
<View style={styles.pointsMonthlyContainer}>
{/* 积分每月显示 */}
<View style={styles.pointsMonthlyCard}>
<View style={styles.pointsMonthlyHeader}>
<MembershipPointsIcon />
<Text style={styles.pointsMonthlyValue}>
{currentPlan.points.toLocaleString()}
</Text>
<Text style={styles.pointsMonthlyLabel}>{t('membership.pointsPerMonth')}</Text>
</View>
<View style={styles.progressBar}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[styles.progressFill, { width: `${progressPercentage}%` }]}
/>
</View>
<Text style={styles.pointsMonthlyNote}>{t('membership.pointsAutoRenew')}</Text>
</View>
{/* 功能列表 */}
<View >
{currentPlan.features.map((feature, index) => (
<View key={index} style={styles.featureItem}>
<CheckMarkIcon />
<Text style={styles.featureText}>{feature}</Text>
<View style={styles.pointsMonthlyContainer}>
{/* 积分每月显示 */}
<View style={styles.pointsMonthlyCard}>
<View style={styles.pointsMonthlyHeader}>
<MembershipPointsIcon />
<Text style={styles.pointsMonthlyValue}>
{currentPlan.points.toLocaleString()}
</Text>
<Text style={styles.pointsMonthlyLabel}>{t('membership.pointsPerMonth')}</Text>
</View>
))}
<View style={styles.progressBar}>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[styles.progressFill, { width: `${progressPercentage}%` }]}
/>
</View>
<Text style={styles.pointsMonthlyNote}>{t('membership.pointsAutoRenew')}</Text>
</View>
{/* 功能列表 */}
<View >
{currentPlan.featureList.map((featureKey, index) => {
const translatedText = t(featureKey)
// 如果没有对应的翻译键(翻译结果等于键本身),不展示该项
if (translatedText === featureKey) {
return null
}
return (
<View key={index} style={styles.featureItem}>
<CheckMarkIcon />
<Text style={styles.featureText}>{translatedText}</Text>
</View>
)
})}
</View>
</View>
</View>
</ScrollView>
{/* 固定在底部的订阅容器 */}
<View style={[styles.subscribeContainer, { paddingBottom: Math.max(insets.bottom, 3) }]}>
{/* 立即开通按钮 */}
<Pressable
disabled={!agreed || isLoadingSubscriptions || isStripePricingLoading || isSubscribing}
style={styles.subscribeButtonPressable}
onPress={handleSubscribe}
disabled={!agreed || isLoadingSubscriptions || isStripePricingLoading || isSubscribing}
style={styles.subscribeButtonPressable}
onPress={handleSubscribe}
>
<LinearGradient
<LinearGradient
colors={currentPlan.recommended
? ['#9966FF', '#FF6699', '#FF9966']
: ['#FFE7F8', '#C6A7FA', '#ADBCFF', '#E6E3FF']
@@ -419,15 +427,15 @@ export default function MembershipScreen() {
(!agreed || isLoadingSubscriptions || isStripePricingLoading || isSubscribing) && styles.subscribeButtonDisabled,
]}
>
{isLoadingSubscriptions || isStripePricingLoading || isSubscribing ? (
<ActivityIndicator color="#F5F5F5" />
) : (
<Text style={currentPlan.recommended ? styles.subscribeButtonText : styles.subscribeButtonText1}>{t('membership.subscribeNow')}</Text>
)}
{isLoadingSubscriptions || isStripePricingLoading || isSubscribing ? (
<ActivityIndicator color="#F5F5F5" />
) : (
<Text style={currentPlan.recommended ? styles.subscribeButtonText : styles.subscribeButtonText1}>{t('membership.subscribeNow')}</Text>
)}
</LinearGradient>
</Pressable>
{/* 协议复选框 */}
<View style={styles.agreementContainer}>
{/* 协议复选框 */}
<View style={styles.agreementContainer}>
<Pressable
style={styles.checkbox}
onPress={() => setAgreed(!agreed)}
@@ -437,14 +445,14 @@ export default function MembershipScreen() {
<View style={styles.agreementTextWrapper}>
<Text style={styles.agreementText}>
{t('membership.agreementText')}{' '}
<Text
<Text
style={styles.agreementLink}
onPress={() => router.push('/terms')}
>
{t('membership.terms')}
</Text>
<Text style={styles.agreementText}> {t('membership.agreementAnd')} </Text>
<Text
<Text
style={styles.agreementLink}
onPress={() => router.push('/privacy')}
>
@@ -455,7 +463,7 @@ export default function MembershipScreen() {
</View>
</View>
</View>
{/* 积分抽屉 */}
<PointsDrawer
visible={pointsDrawerVisible}
@@ -721,15 +729,18 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
marginTop: 8,
marginBottom: 16,
paddingVertical: 8,
},
agreementTextWrapper: {
marginLeft: 4,
},
checkbox: {
width: 12,
height: 12,
width: 20,
height: 20,
alignItems: 'center',
justifyContent: 'center',
padding: 4,
},
agreementText: {
color: '#CCCCCC',