fix: bug
This commit is contained in:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user