diff --git a/src/components/Subscription/SubscriptionContentNew.tsx b/src/components/Subscription/SubscriptionContentNew.tsx index 1d13a5ab..00fb6315 100644 --- a/src/components/Subscription/SubscriptionContentNew.tsx +++ b/src/components/Subscription/SubscriptionContentNew.tsx @@ -53,6 +53,7 @@ export default function SubscriptionContentNew() { const [selectedCycle, setSelectedCycle] = useState('yearly'); const [selectedPlan, setSelectedPlan] = useState(null); + const [subscriptionPlans, setSubscriptionPlans] = useState([]); const [priceInfo, setPriceInfo] = useState(null); const [loading, setLoading] = useState(false); const [promoCode, setPromoCode] = useState(''); @@ -94,6 +95,46 @@ export default function SubscriptionContentNew() { }; }, []); + // 组件加载时获取套餐数据 + useEffect(() => { + fetchSubscriptionPlans(); + }, []); + + const fetchSubscriptionPlans = async () => { + try { + logger.debug('SubscriptionContentNew', '正在获取订阅套餐'); + const response = await fetch('/api/subscription/plans'); + + if (response.ok) { + const data = await response.json(); + + if (data.success && Array.isArray(data.data)) { + const validPlans = data.data.filter( + (plan: any) => + plan && + plan.name && + typeof plan.monthly_price === 'number' && + typeof plan.yearly_price === 'number' + ); + logger.debug('SubscriptionContentNew', '套餐加载成功', { + status: response.status, + validPlansCount: validPlans.length, + }); + setSubscriptionPlans(validPlans); + } else { + logger.warn('SubscriptionContentNew', '套餐数据格式异常', { data }); + setSubscriptionPlans([]); + } + } else { + logger.error('SubscriptionContentNew', 'fetchSubscriptionPlans', new Error(`HTTP ${response.status}`)); + setSubscriptionPlans([]); + } + } catch (error) { + logger.error('SubscriptionContentNew', 'fetchSubscriptionPlans', error); + setSubscriptionPlans([]); + } + }; + const handlePaymentExpire = () => { stopAutoPaymentCheck(); toast({ @@ -377,13 +418,63 @@ export default function SubscriptionContentNew() { } }; + // 合并数据库数据和前端配置 + const getMergedPlans = () => { + // 如果数据库还没有加载数据,使用静态配置 + if (subscriptionPlans.length === 0) { + return subscriptionConfig.plans; + } + + // 合并数据库价格和前端UI配置 + return subscriptionConfig.plans.map((configPlan: any) => { + const dbPlan = subscriptionPlans.find((p: any) => p.name === configPlan.name); + + if (!dbPlan) { + return configPlan; // 如果数据库中没有,使用前端配置 + } + + // 解析数据库中的 pricing_options JSON + let pricingOptions = configPlan.pricingOptions; + if (dbPlan.pricing_options) { + try { + const parsedOptions = typeof dbPlan.pricing_options === 'string' + ? JSON.parse(dbPlan.pricing_options) + : dbPlan.pricing_options; + + if (Array.isArray(parsedOptions) && parsedOptions.length > 0) { + pricingOptions = parsedOptions.map((opt: any) => ({ + cycleKey: opt.cycle_key, + label: opt.label, + months: opt.months, + price: parseFloat(opt.price), + originalPrice: opt.original_price ? parseFloat(opt.original_price) : null, + discountPercent: opt.discount_percent || 0, + })); + } + } catch (error) { + logger.error('SubscriptionContentNew', '解析pricing_options失败', error); + } + } + + // 合并数据,数据库价格优先 + return { + ...configPlan, + monthly_price: dbPlan.monthly_price, + yearly_price: dbPlan.yearly_price, + pricingOptions: pricingOptions, + displayName: dbPlan.display_name || configPlan.displayName, + description: dbPlan.description || configPlan.description, + }; + }); + }; + const getCurrentPrice = (plan: any) => { if (!plan || plan.name === 'free') return 0; const option = plan.pricingOptions?.find( (opt: any) => opt.cycleKey === selectedCycle ); - return option ? option.price : plan.pricingOptions[0]?.price || 0; + return option ? option.price : plan.pricingOptions?.[0]?.price || 0; }; const getCurrentPriceOption = (plan: any) => { @@ -488,7 +579,7 @@ export default function SubscriptionContentNew() { flexWrap="wrap" justify="center" > - {subscriptionConfig.plans[1]?.pricingOptions?.map((option: any, index: number) => ( + {getMergedPlans()[1]?.pricingOptions?.map((option: any, index: number) => ( {option.discountPercent > 0 && ( {(() => { - const currentOption = subscriptionConfig.plans[1]?.pricingOptions?.find( + const currentOption = getMergedPlans()[1]?.pricingOptions?.find( (opt: any) => opt.cycleKey === selectedCycle ); if (currentOption && currentOption.discountPercent > 0) { @@ -560,7 +651,7 @@ export default function SubscriptionContentNew() { maxW="1200px" mx="auto" > - {subscriptionConfig.plans.slice(1).map((plan: any, index: number) => { + {getMergedPlans().slice(1).map((plan: any, index: number) => { const IconComponent = getIconComponent(plan.icon); const currentPriceOption = getCurrentPriceOption(plan); const isCurrentPlan =