Add Chinese translations for medication management and personal settings
- Introduced new translation files for medication, personal, and weight management in Chinese. - Updated the main index file to include the new translation modules. - Enhanced the medication type definitions to include 'ointment'. - Refactored workout type labels to utilize i18n for better localization support. - Improved sleep quality descriptions and recommendations with i18n integration.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import CustomCheckBox from '@/components/ui/CheckBox';
|
||||
import { USER_AGREEMENT_URL } from '@/constants/Agree';
|
||||
import { useAppDispatch } from '@/hooks/redux';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import {
|
||||
MEMBERSHIP_PLAN_META,
|
||||
extractMembershipProductsFromOfferings,
|
||||
@@ -65,51 +66,6 @@ interface BenefitItem {
|
||||
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: '基础提醒'
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const PLAN_STYLE_CONFIG: Record<MembershipPlanType, { gradient: readonly [string, string]; accent: string }> = {
|
||||
lifetime: {
|
||||
@@ -151,6 +107,7 @@ const getPermissionIcon = (type: PermissionType, isVip: boolean) => {
|
||||
};
|
||||
|
||||
export function MembershipModal({ visible, onClose, onPurchaseSuccess }: MembershipModalProps) {
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch();
|
||||
const [selectedProduct, setSelectedProduct] = useState<PurchasesStoreProduct | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -165,6 +122,80 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 保存监听器引用,用于移除监听器
|
||||
const purchaseListenerRef = useRef<((customerInfo: CustomerInfo) => void) | null>(null);
|
||||
|
||||
// 权益对比配置 - Move inside component to use t function
|
||||
const benefitComparison: BenefitItem[] = [
|
||||
{
|
||||
title: t('membershipModal.benefits.items.aiCalories.title'),
|
||||
description: t('membershipModal.benefits.items.aiCalories.description'),
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: t('membershipModal.benefits.permissions.unlimited'),
|
||||
vipText: t('membershipModal.benefits.permissions.unlimited')
|
||||
},
|
||||
regular: {
|
||||
type: 'limited',
|
||||
text: t('membershipModal.benefits.permissions.limited'),
|
||||
vipText: t('membershipModal.benefits.permissions.dailyLimit', { count: 3 })
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('membershipModal.benefits.items.aiNutrition.title'),
|
||||
description: t('membershipModal.benefits.items.aiNutrition.description'),
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: t('membershipModal.benefits.permissions.unlimited'),
|
||||
vipText: t('membershipModal.benefits.permissions.unlimited')
|
||||
},
|
||||
regular: {
|
||||
type: 'limited',
|
||||
text: t('membershipModal.benefits.permissions.limited'),
|
||||
vipText: t('membershipModal.benefits.permissions.dailyLimit', { count: 5 })
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('membershipModal.benefits.items.healthReminder.title'),
|
||||
description: t('membershipModal.benefits.items.healthReminder.description'),
|
||||
vip: {
|
||||
type: 'unlimited',
|
||||
text: t('membershipModal.benefits.permissions.fullSupport'),
|
||||
vipText: t('membershipModal.benefits.permissions.smartReminder')
|
||||
},
|
||||
regular: {
|
||||
type: 'unlimited',
|
||||
text: t('membershipModal.benefits.permissions.basicSupport'),
|
||||
vipText: t('membershipModal.benefits.permissions.basicSupport')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('membershipModal.benefits.items.aiMedication.title'),
|
||||
description: t('membershipModal.benefits.items.aiMedication.description'),
|
||||
vip: {
|
||||
type: 'exclusive',
|
||||
text: t('membershipModal.benefits.permissions.fullAnalysis'),
|
||||
vipText: t('membershipModal.benefits.permissions.fullAnalysis')
|
||||
},
|
||||
regular: {
|
||||
type: 'exclusive',
|
||||
text: t('membershipModal.benefits.permissions.notSupported'),
|
||||
vipText: t('membershipModal.benefits.permissions.notSupported')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('membershipModal.benefits.items.customChallenge.title'),
|
||||
description: t('membershipModal.benefits.items.customChallenge.description'),
|
||||
vip: {
|
||||
type: 'exclusive',
|
||||
text: t('membershipModal.benefits.permissions.createUnlimited'),
|
||||
vipText: t('membershipModal.benefits.permissions.createUnlimited')
|
||||
},
|
||||
regular: {
|
||||
type: 'exclusive',
|
||||
text: t('membershipModal.benefits.permissions.notSupported'),
|
||||
vipText: t('membershipModal.benefits.permissions.notSupported')
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// 根据选中的产品生成tips内容
|
||||
const getTipsContent = (product: PurchasesStoreProduct | null): string => {
|
||||
if (!product) return '';
|
||||
@@ -176,11 +207,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
|
||||
switch (plan.type) {
|
||||
case 'lifetime':
|
||||
return '终身陪伴,见证您的每一次健康蜕变';
|
||||
return t('membershipModal.plans.lifetime.subtitle');
|
||||
case 'quarterly':
|
||||
return '3个月科学计划,让健康成为生活习惯';
|
||||
return t('membershipModal.plans.quarterly.subtitle');
|
||||
case 'weekly':
|
||||
return '7天体验期,感受专业健康指导的力量';
|
||||
return t('membershipModal.plans.weekly.subtitle');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -326,7 +357,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
|
||||
// 显示成功提示
|
||||
GlobalToast.show({
|
||||
message: '会员开通成功',
|
||||
message: t('membershipModal.success.purchase'),
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
@@ -492,11 +523,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 验证是否已同意协议
|
||||
if (!agreementAccepted) {
|
||||
Alert.alert(
|
||||
'请阅读并同意相关协议',
|
||||
'购买前需要同意用户协议、会员协议和自动续费协议',
|
||||
t('membershipModal.agreements.alert.title'),
|
||||
t('membershipModal.agreements.alert.message'),
|
||||
[
|
||||
{
|
||||
text: '确定',
|
||||
text: t('membershipModal.agreements.alert.confirm'),
|
||||
style: 'default',
|
||||
}
|
||||
]
|
||||
@@ -517,11 +548,11 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 验证是否选择了产品
|
||||
if (!selectedProduct) {
|
||||
Alert.alert(
|
||||
'请选择会员套餐',
|
||||
t('membershipModal.errors.selectPlan'),
|
||||
'',
|
||||
[
|
||||
{
|
||||
text: '确定',
|
||||
text: t('membershipModal.agreements.alert.confirm'),
|
||||
style: 'default',
|
||||
}
|
||||
]
|
||||
@@ -579,32 +610,32 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
if (error.userCancelled || error.code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
|
||||
// 用户取消购买
|
||||
GlobalToast.show({
|
||||
message: '购买已取消',
|
||||
message: t('membershipModal.errors.purchaseCancelled'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.PRODUCT_ALREADY_PURCHASED_ERROR) {
|
||||
// 商品已拥有
|
||||
GlobalToast.show({
|
||||
message: '您已拥有此商品',
|
||||
message: t('membershipModal.errors.alreadyPurchased'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR) {
|
||||
// 网络错误
|
||||
GlobalToast.show({
|
||||
message: '网络连接失败',
|
||||
message: t('membershipModal.errors.networkError'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR) {
|
||||
// 支付待处理
|
||||
GlobalToast.show({
|
||||
message: '支付正在处理中',
|
||||
message: t('membershipModal.errors.paymentPending'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR) {
|
||||
// 凭据无效
|
||||
GlobalToast.show({
|
||||
message: '账户验证失败',
|
||||
message: t('membershipModal.errors.invalidCredentials'),
|
||||
});
|
||||
} else {
|
||||
// 其他错误
|
||||
GlobalToast.show({
|
||||
message: '购买失败',
|
||||
message: t('membershipModal.errors.purchaseFailed'),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
@@ -701,7 +732,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onClose?.();
|
||||
|
||||
GlobalToast.show({
|
||||
message: '恢复购买成功',
|
||||
message: t('membershipModal.errors.restoreSuccess'),
|
||||
});
|
||||
|
||||
} catch (apiError: any) {
|
||||
@@ -720,7 +751,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 即使后台接口失败,也显示恢复成功(因为 RevenueCat 已经确认有购买记录)
|
||||
// 但不关闭弹窗,让用户知道可能需要重试
|
||||
GlobalToast.show({
|
||||
message: '恢复购买部分失败',
|
||||
message: t('membershipModal.errors.restorePartialFailed'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -734,7 +765,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
activeSubscriptionsCount: activeSubscriptionIds.length
|
||||
});
|
||||
GlobalToast.show({
|
||||
message: '没有找到购买记录',
|
||||
message: t('membershipModal.errors.noPurchasesFound'),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -754,19 +785,19 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
// 处理特定的恢复购买错误
|
||||
if (error.userCancelled || error.code === Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
|
||||
GlobalToast.show({
|
||||
message: '恢复购买已取消',
|
||||
message: t('membershipModal.errors.restoreCancelled'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.NETWORK_ERROR) {
|
||||
GlobalToast.show({
|
||||
message: '网络错误',
|
||||
message: t('membershipModal.errors.networkError'),
|
||||
});
|
||||
} else if (error.code === Purchases.PURCHASES_ERROR_CODE.INVALID_CREDENTIALS_ERROR) {
|
||||
GlobalToast.show({
|
||||
message: '账户验证失败',
|
||||
message: t('membershipModal.errors.invalidCredentials'),
|
||||
});
|
||||
} else {
|
||||
GlobalToast.show({
|
||||
message: '恢复购买失败',
|
||||
message: t('membershipModal.errors.restoreFailed'),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
@@ -780,7 +811,19 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
const renderPlanCard = (product: PurchasesStoreProduct) => {
|
||||
const planMeta = getPlanMetaById(product.identifier);
|
||||
const isSelected = selectedProduct === product;
|
||||
const displayTitle = resolvePlanDisplayName(product, planMeta);
|
||||
|
||||
// 优先使用翻译的标题,如果找不到 meta 则回退到产品标题
|
||||
let displayTitle = product.title;
|
||||
let displaySubtitle = planMeta?.subtitle ?? '';
|
||||
|
||||
if (planMeta) {
|
||||
displayTitle = t(`membershipModal.plans.${planMeta.type}.title`);
|
||||
displaySubtitle = t(`membershipModal.plans.${planMeta.type}.subtitle`);
|
||||
} else {
|
||||
// 如果没有 meta,尝试使用 resolvePlanDisplayName (虽然这里主要依赖 meta)
|
||||
displayTitle = resolvePlanDisplayName(product, planMeta);
|
||||
}
|
||||
|
||||
const priceLabel = product.priceString || '';
|
||||
const styleConfig = planMeta ? PLAN_STYLE_CONFIG[planMeta.type] : undefined;
|
||||
|
||||
@@ -797,7 +840,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
activeOpacity={loading ? 1 : 0.8}
|
||||
accessible={true}
|
||||
accessibilityLabel={`${displayTitle} ${priceLabel}`}
|
||||
accessibilityHint={loading ? '购买进行中,无法切换套餐' : `选择${displayTitle}套餐`}
|
||||
accessibilityHint={loading ? t('membershipModal.loading.purchase') : t('membershipModal.actions.selectPlan', { plan: displayTitle })}
|
||||
accessibilityState={{ disabled: loading, selected: isSelected }}
|
||||
>
|
||||
<LinearGradient
|
||||
@@ -809,7 +852,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<View style={styles.planCardTopSection}>
|
||||
{planMeta?.tag && (
|
||||
<View style={styles.planTag}>
|
||||
<Text style={styles.planTagText}>{planMeta.tag}</Text>
|
||||
<Text style={styles.planTagText}>{t('membershipModal.plans.tag')}</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.planCardTitle}>{displayTitle}</Text>
|
||||
@@ -825,7 +868,7 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
</View>
|
||||
|
||||
<View style={styles.planCardBottomSection}>
|
||||
<Text style={styles.planCardDescription}>{planMeta?.subtitle ?? ''}</Text>
|
||||
<Text style={styles.planCardDescription}>{displaySubtitle}</Text>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
@@ -854,8 +897,8 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onPress={onClose}
|
||||
activeOpacity={0.7}
|
||||
accessible={true}
|
||||
accessibilityLabel="返回"
|
||||
accessibilityHint="关闭会员购买弹窗"
|
||||
accessibilityLabel={t('membershipModal.actions.back')}
|
||||
accessibilityHint={t('membershipModal.actions.close')}
|
||||
style={styles.floatingBackButtonContainer}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
@@ -887,14 +930,14 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<View style={styles.sectionTitleBadge}>
|
||||
<Ionicons name="star" size={16} color="#7B2CBF" />
|
||||
</View>
|
||||
<Text style={styles.sectionTitle}>会员套餐</Text>
|
||||
<Text style={styles.sectionTitle}>{t('membershipModal.sectionTitle.plans')}</Text>
|
||||
</View>
|
||||
<Text style={styles.sectionSubtitle}>灵活选择,跟随节奏稳步提升</Text>
|
||||
<Text style={styles.sectionSubtitle}>{t('membershipModal.sectionTitle.plansSubtitle')}</Text>
|
||||
|
||||
{products.length === 0 ? (
|
||||
<View style={styles.configurationNotice}>
|
||||
<Text style={styles.configurationText}>
|
||||
暂未获取到会员商品,请在 RevenueCat 中配置 iOS 产品并同步到当前 Offering。
|
||||
{t('membershipModal.errors.noProducts')}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
@@ -917,17 +960,17 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
<View style={styles.sectionTitleBadge}>
|
||||
<Ionicons name="checkbox" size={16} color="#FF9F0A" />
|
||||
</View>
|
||||
<Text style={styles.sectionTitle}>权益对比</Text>
|
||||
<Text style={styles.sectionTitle}>{t('membershipModal.benefits.title')}</Text>
|
||||
</View>
|
||||
<Text style={styles.sectionSubtitle}>核心权益一目了然,选择更安心</Text>
|
||||
<Text style={styles.sectionSubtitle}>{t('membershipModal.benefits.subtitle')}</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>
|
||||
<Text style={[styles.tableHeaderText, styles.tableTitleCell]}>{t('membershipModal.benefits.table.benefit')}</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableVipCell]}>{t('membershipModal.benefits.table.vip')}</Text>
|
||||
<Text style={[styles.tableHeaderText, styles.tableNormalCell]}>{t('membershipModal.benefits.table.regular')}</Text>
|
||||
</View>
|
||||
{BENEFIT_COMPARISON.map((row, index) => (
|
||||
{benefitComparison.map((row, index) => (
|
||||
<View
|
||||
key={row.title}
|
||||
style={[
|
||||
@@ -963,39 +1006,46 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
</View>
|
||||
|
||||
<View style={styles.bottomSection}>
|
||||
<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 style={styles.agreementContainer}>
|
||||
<View style={styles.checkboxWrapper}>
|
||||
<CustomCheckBox
|
||||
checked={agreementAccepted}
|
||||
onCheckedChange={setAgreementAccepted}
|
||||
size={16}
|
||||
checkedColor="#E91E63"
|
||||
uncheckedColor="#999"
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.agreementText}>
|
||||
{t('membershipModal.agreements.prefix')}
|
||||
<Text
|
||||
style={styles.agreementLink}
|
||||
onPress={() => {
|
||||
Linking.openURL(USER_AGREEMENT_URL);
|
||||
captureMessage('click user agreement');
|
||||
}}
|
||||
>
|
||||
{t('membershipModal.agreements.userAgreement')}
|
||||
</Text>
|
||||
<Text style={styles.agreementSeparator}> | </Text>
|
||||
<Text
|
||||
style={styles.agreementLink}
|
||||
onPress={() => {
|
||||
captureMessage('click membership agreement');
|
||||
}}
|
||||
>
|
||||
{t('membershipModal.agreements.membershipAgreement')}
|
||||
</Text>
|
||||
<Text style={styles.agreementSeparator}> | </Text>
|
||||
<Text
|
||||
style={styles.agreementLink}
|
||||
onPress={() => {
|
||||
captureMessage('click auto renewal agreement');
|
||||
}}
|
||||
>
|
||||
{t('membershipModal.agreements.autoRenewalAgreement')}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
@@ -1006,10 +1056,10 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
{restoring ? (
|
||||
<View style={styles.restoreButtonContent}>
|
||||
<ActivityIndicator size="small" color="#666" style={styles.restoreButtonLoader} />
|
||||
<Text style={styles.restoreButtonText}>恢复中...</Text>
|
||||
<Text style={styles.restoreButtonText}>{t('membershipModal.actions.restoring')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.restoreButtonText}>恢复购买</Text>
|
||||
<Text style={styles.restoreButtonText}>{t('membershipModal.actions.restore')}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -1031,15 +1081,15 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onPress={handlePurchase}
|
||||
disabled={loading || products.length === 0}
|
||||
accessible={true}
|
||||
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
|
||||
accessibilityLabel={loading ? t('membershipModal.actions.processing') : t('membershipModal.actions.subscribe')}
|
||||
accessibilityHint={
|
||||
loading
|
||||
? '购买正在进行中,请稍候'
|
||||
? t('membershipModal.loading.purchase')
|
||||
: products.length === 0
|
||||
? '正在加载会员套餐,请稍候'
|
||||
? t('membershipModal.loading.products')
|
||||
: !selectedProduct
|
||||
? '请选择会员套餐后再进行购买'
|
||||
: `点击购买${selectedProduct.title || '已选'}会员套餐`
|
||||
? t('membershipModal.errors.selectPlan')
|
||||
: t('membershipModal.actions.purchaseHint', { plan: selectedProduct.title || '已选' })
|
||||
}
|
||||
accessibilityState={{ disabled: loading || products.length === 0 }}
|
||||
style={styles.purchaseButtonContent}
|
||||
@@ -1047,10 +1097,10 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
|
||||
<Text style={styles.purchaseButtonText}>正在处理购买...</Text>
|
||||
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.processing')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.purchaseButtonText}>立即订阅</Text>
|
||||
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.subscribe')}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</GlassView>
|
||||
@@ -1066,15 +1116,15 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
onPress={handlePurchase}
|
||||
disabled={loading || products.length === 0}
|
||||
accessible={true}
|
||||
accessibilityLabel={loading ? '正在处理购买' : '购买会员'}
|
||||
accessibilityLabel={loading ? t('membershipModal.actions.processing') : t('membershipModal.actions.subscribe')}
|
||||
accessibilityHint={
|
||||
loading
|
||||
? '购买正在进行中,请稍候'
|
||||
? t('membershipModal.loading.purchase')
|
||||
: products.length === 0
|
||||
? '正在加载会员套餐,请稍候'
|
||||
? t('membershipModal.loading.products')
|
||||
: !selectedProduct
|
||||
? '请选择会员套餐后再进行购买'
|
||||
: `点击购买${selectedProduct.title || '已选'}会员套餐`
|
||||
? t('membershipModal.errors.selectPlan')
|
||||
: t('membershipModal.actions.purchaseHint', { plan: selectedProduct.title || '已选' })
|
||||
}
|
||||
accessibilityState={{ disabled: loading || products.length === 0 }}
|
||||
style={styles.purchaseButtonContent}
|
||||
@@ -1082,10 +1132,10 @@ export function MembershipModal({ visible, onClose, onPurchaseSuccess }: Members
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" color="white" style={styles.loadingSpinner} />
|
||||
<Text style={styles.purchaseButtonText}>正在处理购买...</Text>
|
||||
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.processing')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.purchaseButtonText}>立即订阅</Text>
|
||||
<Text style={styles.purchaseButtonText}>{t('membershipModal.actions.subscribe')}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -1168,12 +1218,14 @@ const styles = StyleSheet.create({
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#2B2B2E',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
sectionSubtitle: {
|
||||
fontSize: 13,
|
||||
color: '#6B6B73',
|
||||
marginTop: 6,
|
||||
marginBottom: 16,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
configurationNotice: {
|
||||
borderRadius: 16,
|
||||
@@ -1185,6 +1237,7 @@ const styles = StyleSheet.create({
|
||||
color: '#B86A04',
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
plansContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -1217,35 +1270,40 @@ const styles = StyleSheet.create({
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: '#2F2F36',
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 6,
|
||||
marginBottom: 12,
|
||||
},
|
||||
planTagText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 11,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
planCardTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#241F1F',
|
||||
},
|
||||
planCardPrice: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#241F1F',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
planCardPrice: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
marginTop: 12,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
planCardOriginalPrice: {
|
||||
fontSize: 13,
|
||||
color: '#8E8EA1',
|
||||
textDecorationLine: 'line-through',
|
||||
marginTop: 2,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
planCardDescription: {
|
||||
fontSize: 12,
|
||||
color: '#6C6C77',
|
||||
lineHeight: 17,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
planCardTopSection: {
|
||||
flex: 1,
|
||||
@@ -1275,6 +1333,7 @@ const styles = StyleSheet.create({
|
||||
color: '#9B6200',
|
||||
marginLeft: 6,
|
||||
lineHeight: 16,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
comparisonTable: {
|
||||
borderRadius: 16,
|
||||
@@ -1298,10 +1357,12 @@ const styles = StyleSheet.create({
|
||||
color: '#575764',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.4,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
tableCellText: {
|
||||
fontSize: 13,
|
||||
color: '#3E3E44',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
tableTitleCell: {
|
||||
flex: 1.5,
|
||||
@@ -1361,6 +1422,7 @@ const styles = StyleSheet.create({
|
||||
color: '#FFFFFF',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
loadingContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -1369,29 +1431,34 @@ const styles = StyleSheet.create({
|
||||
loadingSpinner: {
|
||||
marginRight: 8,
|
||||
},
|
||||
agreementRow: {
|
||||
agreementContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
marginBottom: 16,
|
||||
marginBottom: 20,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
agreementPrefix: {
|
||||
fontSize: 10,
|
||||
checkboxWrapper: {
|
||||
marginTop: 2, // Align with text line-height
|
||||
marginRight: 8,
|
||||
},
|
||||
agreementText: {
|
||||
flex: 1,
|
||||
fontSize: 11,
|
||||
lineHeight: 16,
|
||||
color: '#666672',
|
||||
marginRight: 4,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
agreementLink: {
|
||||
fontSize: 10,
|
||||
fontSize: 11,
|
||||
color: '#E91E63',
|
||||
textDecorationLine: 'underline',
|
||||
fontWeight: '500',
|
||||
marginHorizontal: 2,
|
||||
textDecorationLine: 'underline',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
agreementSeparator: {
|
||||
fontSize: 10,
|
||||
fontSize: 11,
|
||||
color: '#A0A0B0',
|
||||
marginHorizontal: 2,
|
||||
},
|
||||
restoreButton: {
|
||||
alignSelf: 'center',
|
||||
@@ -1401,6 +1468,7 @@ const styles = StyleSheet.create({
|
||||
color: '#6F6F7A',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
disabledRestoreButton: {
|
||||
opacity: 0.5,
|
||||
@@ -1422,6 +1490,7 @@ const styles = StyleSheet.create({
|
||||
color: '#8E8E93',
|
||||
marginTop: 2,
|
||||
lineHeight: 14,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
permissionContainer: {
|
||||
alignItems: 'center',
|
||||
@@ -1435,5 +1504,6 @@ const styles = StyleSheet.create({
|
||||
marginTop: 4,
|
||||
textAlign: 'center',
|
||||
lineHeight: 12,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useAppSelector } from '@/hooks/redux';
|
||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { Image } from 'expo-image';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -15,11 +20,11 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useAppSelector } from '@/hooks/redux';
|
||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
|
||||
const CTA_GRADIENT: [string, string] = ['#5E8BFF', '#6B6CFF'];
|
||||
const CTA_DISABLED_GRADIENT: [string, string] = ['#d3d7e8', '#c1c6da'];
|
||||
|
||||
export interface CreateCustomFoodModalProps {
|
||||
visible: boolean;
|
||||
@@ -43,9 +48,10 @@ export function CreateCustomFoodModal({
|
||||
onClose,
|
||||
onSave
|
||||
}: CreateCustomFoodModalProps) {
|
||||
const { t } = useI18n();
|
||||
const [foodName, setFoodName] = useState('');
|
||||
const [defaultAmount, setDefaultAmount] = useState('100');
|
||||
const [caloriesUnit, setCaloriesUnit] = useState('千卡');
|
||||
const [caloriesUnit, setCaloriesUnit] = useState(t('createCustomFood.units.kcal'));
|
||||
const [calories, setCalories] = useState('100');
|
||||
const [imageUrl, setImageUrl] = useState<string>('');
|
||||
const [protein, setProtein] = useState('0');
|
||||
@@ -93,7 +99,7 @@ export function CreateCustomFoodModal({
|
||||
if (visible) {
|
||||
setFoodName('');
|
||||
setDefaultAmount('100');
|
||||
setCaloriesUnit('千卡');
|
||||
setCaloriesUnit(t('createCustomFood.units.kcal'));
|
||||
setCalories('100');
|
||||
setImageUrl('');
|
||||
setProtein('0');
|
||||
@@ -102,16 +108,16 @@ export function CreateCustomFoodModal({
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
// 选择热量单位
|
||||
|
||||
|
||||
// 选择图片
|
||||
const handleSelectImage = async () => {
|
||||
try {
|
||||
const resp = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited';
|
||||
if (!libGranted) {
|
||||
Alert.alert('权限不足', '需要相册权限以选择照片');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.permissionDenied.title'),
|
||||
t('createCustomFood.alerts.permissionDenied.message')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,11 +143,17 @@ export function CreateCustomFoodModal({
|
||||
setImageUrl(url);
|
||||
} catch (e) {
|
||||
console.warn('上传照片失败', e);
|
||||
Alert.alert('上传失败', '照片上传失败,请重试');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.uploadFailed.title'),
|
||||
t('createCustomFood.alerts.uploadFailed.message')
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Alert.alert('发生错误', '选择照片失败,请重试');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.error.title'),
|
||||
t('createCustomFood.alerts.error.message')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -151,12 +163,18 @@ export function CreateCustomFoodModal({
|
||||
// 保存自定义食物
|
||||
const handleSave = () => {
|
||||
if (!foodName.trim()) {
|
||||
Alert.alert('提示', '请输入食物名称');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.validation.title'),
|
||||
t('createCustomFood.alerts.validation.nameRequired')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!calories.trim() || parseFloat(calories) <= 0) {
|
||||
Alert.alert('提示', '请输入有效的热量值');
|
||||
Alert.alert(
|
||||
t('createCustomFood.alerts.validation.title'),
|
||||
t('createCustomFood.alerts.validation.caloriesRequired')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -175,75 +193,99 @@ export function CreateCustomFoodModal({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const isSaveDisabled = !foodName.trim() || !calories.trim();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="fade"
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={onClose}
|
||||
presentationStyle="overFullScreen"
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<BlurView intensity={20} tint="dark" style={styles.overlay}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.keyboardAvoidingView}
|
||||
>
|
||||
<View style={[
|
||||
styles.modalContainer,
|
||||
keyboardHeight > 0 && {
|
||||
height: screenHeight - keyboardHeight,
|
||||
maxHeight: screenHeight - keyboardHeight,
|
||||
}
|
||||
]}>
|
||||
<TouchableOpacity activeOpacity={1} onPress={onClose} style={styles.dismissArea} />
|
||||
<View
|
||||
style={[
|
||||
styles.modalContainer,
|
||||
keyboardHeight > 0 && {
|
||||
height: screenHeight - keyboardHeight - 60,
|
||||
maxHeight: screenHeight - keyboardHeight - 60,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.modalHeaderBar}>
|
||||
<View style={styles.dragIndicator} />
|
||||
</View>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
paddingBottom: keyboardHeight > 0 ? 20 : 0
|
||||
paddingBottom: keyboardHeight > 0 ? 20 : 40,
|
||||
}}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={onClose} style={styles.backButton}>
|
||||
<Ionicons name="chevron-back" size={24} color="#333" />
|
||||
<TouchableOpacity onPress={onClose} style={styles.backButton} activeOpacity={0.7}>
|
||||
<Ionicons name="close-circle" size={32} color="#E2E8F0" />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>创建自定义食物</Text>
|
||||
<Text style={styles.headerTitle}>{t('createCustomFood.title')}</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.saveButton,
|
||||
(!foodName.trim() || !calories.trim()) && styles.saveButtonDisabled
|
||||
]}
|
||||
style={[styles.saveButton, isSaveDisabled && styles.saveButtonDisabled]}
|
||||
onPress={handleSave}
|
||||
disabled={!foodName.trim() || !calories.trim()}
|
||||
disabled={isSaveDisabled}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={[
|
||||
styles.saveButtonText,
|
||||
(!foodName.trim() || !calories.trim()) && styles.saveButtonTextDisabled
|
||||
]}>保存</Text>
|
||||
<LinearGradient
|
||||
colors={isSaveDisabled ? CTA_DISABLED_GRADIENT : CTA_GRADIENT}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.saveButtonGradient}
|
||||
>
|
||||
<Text style={styles.saveButtonText}>{t('createCustomFood.save')}</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 效果预览区域 */}
|
||||
<View style={styles.previewSection}>
|
||||
<Text style={styles.sectionTitle}>效果预览</Text>
|
||||
<View style={styles.previewCard}>
|
||||
<LinearGradient
|
||||
colors={['#ffffff', '#F8F9FF']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
<View style={styles.previewHeader}>
|
||||
<Text style={styles.sectionTitle}>{t('createCustomFood.preview.title')}</Text>
|
||||
</View>
|
||||
<View style={styles.previewContent}>
|
||||
{imageUrl ? (
|
||||
<Image style={styles.previewImage} source={{ uri: imageUrl }} />
|
||||
) : (
|
||||
<View style={styles.previewImagePlaceholder}>
|
||||
<Ionicons name="restaurant" size={20} color="#999" />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.imageWrapper}>
|
||||
{imageUrl ? (
|
||||
<Image style={styles.previewImage} source={{ uri: imageUrl }} />
|
||||
) : (
|
||||
<View style={styles.previewImagePlaceholder}>
|
||||
<Ionicons name="restaurant" size={24} color="#94A3B8" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.previewInfo}>
|
||||
<Text style={styles.previewName}>
|
||||
{foodName || '食物名称'}
|
||||
</Text>
|
||||
<Text style={styles.previewCalories}>
|
||||
{actualCalories}{caloriesUnit}/{defaultAmount}g
|
||||
<Text style={styles.previewName} numberOfLines={1}>
|
||||
{foodName || t('createCustomFood.preview.defaultName')}
|
||||
</Text>
|
||||
<View style={styles.previewBadge}>
|
||||
<Ionicons name="flame" size={14} color="#F59E0B" />
|
||||
<Text style={styles.previewCalories}>
|
||||
{actualCalories} {caloriesUnit} / {defaultAmount}
|
||||
{t('createCustomFood.units.g')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -252,21 +294,21 @@ export function CreateCustomFoodModal({
|
||||
{/* 基本信息 */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>基本信息</Text>
|
||||
<Text style={styles.sectionTitle}>{t('createCustomFood.basicInfo.title')}</Text>
|
||||
<Text style={styles.requiredIndicator}>*</Text>
|
||||
</View>
|
||||
<View style={styles.sectionCard}>
|
||||
{/* 食物名称和单位 */}
|
||||
{/* 食物名称 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>食物名称</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.basicInfo.name')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={foodName}
|
||||
onChangeText={setFoodName}
|
||||
placeholder="例如,汉堡"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholder={t('createCustomFood.basicInfo.namePlaceholder')}
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -274,36 +316,36 @@ export function CreateCustomFoodModal({
|
||||
|
||||
{/* 默认数量 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>默认数量</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.basicInfo.defaultAmount')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={defaultAmount}
|
||||
onChangeText={setDefaultAmount}
|
||||
keyboardType="numeric"
|
||||
placeholder="100"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>g</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.g')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 食物热量 */}
|
||||
<View style={[styles.inputRowContainer, { marginBottom: 0 }]}>
|
||||
<Text style={styles.inputRowLabel}>食物热量</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.basicInfo.calories')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={calories}
|
||||
onChangeText={setCalories}
|
||||
keyboardType="numeric"
|
||||
placeholder="100"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholder="0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>千卡</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.kcal')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -312,23 +354,26 @@ export function CreateCustomFoodModal({
|
||||
|
||||
{/* 可选信息 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>可选信息</Text>
|
||||
<Text style={styles.sectionTitle}>{t('createCustomFood.optionalInfo.title')}</Text>
|
||||
<View style={styles.sectionCard}>
|
||||
{/* 照片 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>照片</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.optionalInfo.photo')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<TouchableOpacity
|
||||
style={styles.modernImageSelector}
|
||||
<TouchableOpacity
|
||||
style={styles.modernImageSelector}
|
||||
onPress={handleSelectImage}
|
||||
disabled={uploading}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{imageUrl ? (
|
||||
<Image style={styles.selectedImage} source={{ uri: imageUrl }} />
|
||||
) : (
|
||||
<View style={styles.modernImagePlaceholder}>
|
||||
<Ionicons name="camera" size={28} color="#A0A0A0" />
|
||||
<Text style={styles.imagePlaceholderText}>添加照片</Text>
|
||||
<Ionicons name="camera-outline" size={28} color="#94A3B8" />
|
||||
<Text style={styles.imagePlaceholderText}>
|
||||
{t('createCustomFood.optionalInfo.addPhoto')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{uploading && (
|
||||
@@ -342,54 +387,56 @@ export function CreateCustomFoodModal({
|
||||
|
||||
{/* 蛋白质 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>蛋白质</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.optionalInfo.protein')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={protein}
|
||||
onChangeText={setProtein}
|
||||
keyboardType="numeric"
|
||||
placeholder="0"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>克</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.gram')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 脂肪 */}
|
||||
<View style={styles.inputRowContainer}>
|
||||
<Text style={styles.inputRowLabel}>脂肪</Text>
|
||||
<Text style={styles.inputRowLabel}>{t('createCustomFood.optionalInfo.fat')}</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={fat}
|
||||
onChangeText={setFat}
|
||||
keyboardType="numeric"
|
||||
placeholder="0"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>克</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.gram')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 碳水化合物 */}
|
||||
<View style={[styles.inputRowContainer, { marginBottom: 0 }]}>
|
||||
<Text style={styles.inputRowLabel}>碳水化合物</Text>
|
||||
<Text style={styles.inputRowLabel}>
|
||||
{t('createCustomFood.optionalInfo.carbohydrate')}
|
||||
</Text>
|
||||
<View style={styles.inputRowContent}>
|
||||
<View style={styles.numberInputContainer}>
|
||||
<View style={styles.modernInputContainer}>
|
||||
<TextInput
|
||||
style={styles.modernNumberInput}
|
||||
value={carbohydrate}
|
||||
onChangeText={setCarbohydrate}
|
||||
keyboardType="numeric"
|
||||
placeholder="0"
|
||||
placeholderTextColor="#A0A0A0"
|
||||
placeholderTextColor="#94A3B8"
|
||||
/>
|
||||
<Text style={styles.unitText}>克</Text>
|
||||
<Text style={styles.unitText}>{t('createCustomFood.units.gram')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -398,7 +445,7 @@ export function CreateCustomFoodModal({
|
||||
</ScrollView>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</BlurView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -408,331 +455,272 @@ const { height: screenHeight } = Dimensions.get('window');
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
keyboardAvoidingView: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
dismissArea: {
|
||||
flex: 1,
|
||||
},
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
marginTop: 50,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
backgroundColor: '#F1F5F9', // Slate 100
|
||||
borderTopLeftRadius: 32,
|
||||
borderTopRightRadius: 32,
|
||||
height: '90%',
|
||||
maxHeight: '90%',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: -4,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 12,
|
||||
elevation: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
modalHeaderBar: {
|
||||
width: '100%',
|
||||
height: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#F1F5F9',
|
||||
},
|
||||
dragIndicator: {
|
||||
width: 40,
|
||||
height: 4,
|
||||
backgroundColor: '#CBD5E1',
|
||||
borderRadius: 2,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F1F5F9',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
backButton: {
|
||||
padding: 4,
|
||||
marginLeft: -8,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
flex: 1,
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
color: '#1E293B',
|
||||
textAlign: 'center',
|
||||
marginHorizontal: 20,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
saveButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 16,
|
||||
borderRadius: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
saveButtonDisabled: {
|
||||
opacity: 0.5,
|
||||
opacity: 0.6,
|
||||
},
|
||||
saveButtonGradient: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
},
|
||||
saveButtonText: {
|
||||
fontSize: 16,
|
||||
color: Colors.light.primary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
saveButtonTextDisabled: {
|
||||
color: Colors.light.textMuted,
|
||||
fontSize: 14,
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '700',
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
previewSection: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 16,
|
||||
paddingHorizontal: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
previewCard: {
|
||||
backgroundColor: '#F8F9FA',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginTop: 8,
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: 'rgba(30, 41, 59, 0.08)',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 12,
|
||||
elevation: 4,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.6)',
|
||||
},
|
||||
previewHeader: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
previewContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
imageWrapper: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
previewImage: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 4,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F8FAFC',
|
||||
},
|
||||
previewImagePlaceholder: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#E5E5E5',
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F1F5F9',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
},
|
||||
previewInfo: {
|
||||
flex: 1,
|
||||
marginLeft: 12,
|
||||
marginLeft: 16,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
previewName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: '#333',
|
||||
marginBottom: 2,
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#1E293B',
|
||||
marginBottom: 6,
|
||||
fontFamily: 'AliBold',
|
||||
},
|
||||
previewBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FFFBEB',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
alignSelf: 'flex-start',
|
||||
gap: 4,
|
||||
},
|
||||
previewCalories: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontSize: 13,
|
||||
color: '#D97706',
|
||||
fontWeight: '600',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
section: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
marginLeft: 8
|
||||
fontWeight: '700',
|
||||
color: '#64748B',
|
||||
fontFamily: 'AliBold',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
requiredIndicator: {
|
||||
fontSize: 16,
|
||||
color: '#FF4444',
|
||||
fontSize: 14,
|
||||
color: '#EF4444',
|
||||
marginLeft: 4,
|
||||
},
|
||||
inputGroup: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputRowGroup: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputRowItem: {
|
||||
flex: 1,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontWeight: '500',
|
||||
},
|
||||
modernTextInput: {
|
||||
flex: 1,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
fontSize: 16,
|
||||
marginLeft: 20,
|
||||
color: '#333',
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
numberInputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 12,
|
||||
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
modernNumberInput: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
textAlign: 'right',
|
||||
},
|
||||
unitText: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
paddingRight: 16,
|
||||
minWidth: 40,
|
||||
textAlign: 'center',
|
||||
},
|
||||
modernSelectButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#E8E8E8',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
selectButtonText: {
|
||||
fontSize: 14,
|
||||
color: 'gray',
|
||||
fontWeight: '500',
|
||||
},
|
||||
modernImageSelector: {
|
||||
alignSelf: 'flex-end',
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
selectedImage: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 16,
|
||||
},
|
||||
modernImagePlaceholder: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F8F8F8',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#E8E8E8',
|
||||
borderStyle: 'dashed',
|
||||
},
|
||||
imagePlaceholderText: {
|
||||
fontSize: 12,
|
||||
color: '#A0A0A0',
|
||||
marginTop: 4,
|
||||
fontWeight: '500',
|
||||
},
|
||||
nutritionRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginBottom: 20,
|
||||
},
|
||||
nutritionItem: {
|
||||
flex: 1,
|
||||
},
|
||||
// 保留旧样式以防兼容性问题
|
||||
textInput: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E5E5',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
numberInput: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E5E5',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
backgroundColor: '#FFFFFF',
|
||||
textAlign: 'right',
|
||||
},
|
||||
inputWithUnit: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
inputUnit: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
minWidth: 30,
|
||||
},
|
||||
selectButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E5E5',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
imageSelector: {
|
||||
alignSelf: 'flex-end',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
imagePlaceholder: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#F0F0F0',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E5E5',
|
||||
},
|
||||
disclaimer: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 20,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
disclaimerText: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
lineHeight: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
sectionCard: {
|
||||
backgroundColor: '#F8F9FA',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginTop: 8,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
shadowColor: 'rgba(30, 41, 59, 0.05)',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 10,
|
||||
elevation: 2,
|
||||
},
|
||||
// 新增行布局样式
|
||||
inputRowContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
marginBottom: 16,
|
||||
},
|
||||
inputRowLabel: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontWeight: '500',
|
||||
width: 80,
|
||||
fontSize: 15,
|
||||
color: '#475569',
|
||||
fontWeight: '600',
|
||||
width: 90,
|
||||
marginRight: 12,
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
inputRowContent: {
|
||||
flex: 1,
|
||||
},
|
||||
imageLoadingOverlay: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
modernInputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F8FAFC',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
modernNumberInput: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
color: '#1E293B',
|
||||
textAlign: 'right',
|
||||
fontFamily: 'AliRegular',
|
||||
},
|
||||
unitText: {
|
||||
fontSize: 14,
|
||||
color: '#94A3B8',
|
||||
paddingRight: 16,
|
||||
minWidth: 40,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
},
|
||||
modernImageSelector: {
|
||||
alignSelf: 'flex-end',
|
||||
borderRadius: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
selectedImage: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 20,
|
||||
},
|
||||
modernImagePlaceholder: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#F8FAFC',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
borderStyle: 'dashed',
|
||||
},
|
||||
imagePlaceholderText: {
|
||||
fontSize: 11,
|
||||
color: '#94A3B8',
|
||||
marginTop: 4,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
imageLoadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||
borderRadius: 20,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user