加入优惠码机制,预置3个优惠码
This commit is contained in:
@@ -29,6 +29,9 @@ import {
|
||||
Td,
|
||||
Heading,
|
||||
Collapse,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { logger } from '../../utils/logger';
|
||||
@@ -85,6 +88,13 @@ export default function SubscriptionContent() {
|
||||
const [forceUpdating, setForceUpdating] = useState(false);
|
||||
const [openFaqIndex, setOpenFaqIndex] = useState(null);
|
||||
|
||||
// 优惠码相关state
|
||||
const [promoCode, setPromoCode] = useState('');
|
||||
const [promoCodeApplied, setPromoCodeApplied] = useState(false);
|
||||
const [promoCodeError, setPromoCodeError] = useState('');
|
||||
const [validatingPromo, setValidatingPromo] = useState(false);
|
||||
const [priceInfo, setPriceInfo] = useState(null); // 价格信息(包含升级计算)
|
||||
|
||||
// 加载订阅套餐数据
|
||||
useEffect(() => {
|
||||
fetchSubscriptionPlans();
|
||||
@@ -149,7 +159,88 @@ export default function SubscriptionContent() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubscribe = (plan) => {
|
||||
// 计算价格(包含升级和优惠码)
|
||||
const calculatePrice = async (plan, cycle, promoCodeValue = null) => {
|
||||
try {
|
||||
const response = await fetch('/api/subscription/calculate-price', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
to_plan: plan.name,
|
||||
to_cycle: cycle,
|
||||
promo_code: promoCodeValue || null
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setPriceInfo(data.data);
|
||||
return data.data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error('SubscriptionContent', 'calculatePrice', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 验证优惠码
|
||||
const handleValidatePromoCode = async () => {
|
||||
if (!promoCode.trim()) {
|
||||
setPromoCodeError('请输入优惠码');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedPlan) {
|
||||
setPromoCodeError('请先选择套餐');
|
||||
return;
|
||||
}
|
||||
|
||||
setValidatingPromo(true);
|
||||
setPromoCodeError('');
|
||||
|
||||
try {
|
||||
// 重新计算价格,包含优惠码
|
||||
const result = await calculatePrice(selectedPlan, selectedCycle, promoCode);
|
||||
|
||||
if (result && !result.promo_error) {
|
||||
setPromoCodeApplied(true);
|
||||
toast({
|
||||
title: '优惠码已应用',
|
||||
description: `节省 ¥${result.discount_amount.toFixed(2)}`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
setPromoCodeError(result?.promo_error || '优惠码无效');
|
||||
setPromoCodeApplied(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setPromoCodeError('验证失败,请重试');
|
||||
setPromoCodeApplied(false);
|
||||
} finally {
|
||||
setValidatingPromo(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 移除优惠码
|
||||
const handleRemovePromoCode = async () => {
|
||||
setPromoCode('');
|
||||
setPromoCodeApplied(false);
|
||||
setPromoCodeError('');
|
||||
// 重新计算价格(不含优惠码)
|
||||
if (selectedPlan) {
|
||||
await calculatePrice(selectedPlan, selectedCycle, null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubscribe = async (plan) => {
|
||||
if (!user) {
|
||||
toast({
|
||||
title: '请先登录',
|
||||
@@ -178,6 +269,10 @@ export default function SubscriptionContent() {
|
||||
);
|
||||
|
||||
setSelectedPlan(plan);
|
||||
|
||||
// 计算价格(包含升级判断)
|
||||
await calculatePrice(plan, selectedCycle, promoCodeApplied ? promoCode : null);
|
||||
|
||||
onPaymentModalOpen();
|
||||
};
|
||||
|
||||
@@ -186,7 +281,7 @@ export default function SubscriptionContent() {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const price = selectedCycle === 'monthly' ? selectedPlan.monthly_price : selectedPlan.yearly_price;
|
||||
const price = priceInfo?.final_amount || (selectedCycle === 'monthly' ? selectedPlan.monthly_price : selectedPlan.yearly_price);
|
||||
|
||||
// 🎯 追踪支付发起
|
||||
subscriptionEvents.trackPaymentInitiated({
|
||||
@@ -205,7 +300,8 @@ export default function SubscriptionContent() {
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
plan_name: selectedPlan.name,
|
||||
billing_cycle: selectedCycle
|
||||
billing_cycle: selectedCycle,
|
||||
promo_code: promoCodeApplied ? promoCode : null
|
||||
})
|
||||
});
|
||||
|
||||
@@ -488,6 +584,27 @@ export default function SubscriptionContent() {
|
||||
return `年付节省 ${percentage}%`;
|
||||
};
|
||||
|
||||
// 获取按钮文字(根据用户当前订阅判断是升级还是新订阅)
|
||||
const getButtonText = (plan, user) => {
|
||||
if (!user || user.subscription_type === 'free') {
|
||||
return `选择 ${plan.display_name}`;
|
||||
}
|
||||
|
||||
// 判断是否为升级
|
||||
const planLevels = { 'free': 0, 'pro': 1, 'max': 2 };
|
||||
const currentLevel = planLevels[user.subscription_type] || 0;
|
||||
const targetLevel = planLevels[plan.name] || 0;
|
||||
|
||||
if (targetLevel > currentLevel) {
|
||||
return `升级至 ${plan.display_name}`;
|
||||
} else if (targetLevel < currentLevel) {
|
||||
return `切换至 ${plan.display_name}`;
|
||||
} else {
|
||||
// 同级别,可能是切换周期
|
||||
return `切换至 ${plan.display_name}`;
|
||||
}
|
||||
};
|
||||
|
||||
// 统一的功能列表定义 - 基于商业定价(10月15日)文档
|
||||
const allFeatures = [
|
||||
// 新闻催化分析模块
|
||||
@@ -838,7 +955,8 @@ export default function SubscriptionContent() {
|
||||
onClick={() => handleSubscribe(plan)}
|
||||
isDisabled={
|
||||
user?.subscription_type === plan.name &&
|
||||
user?.subscription_status === 'active'
|
||||
user?.subscription_status === 'active' &&
|
||||
user?.billing_cycle === selectedCycle
|
||||
}
|
||||
_hover={{
|
||||
transform: 'scale(1.02)',
|
||||
@@ -846,9 +964,10 @@ export default function SubscriptionContent() {
|
||||
transition="all 0.2s"
|
||||
>
|
||||
{user?.subscription_type === plan.name &&
|
||||
user?.subscription_status === 'active'
|
||||
? '✓ 已订阅'
|
||||
: `选择 ${plan.display_name}`
|
||||
user?.subscription_status === 'active' &&
|
||||
user?.billing_cycle === selectedCycle
|
||||
? '✓ 当前套餐'
|
||||
: getButtonText(plan, user)
|
||||
}
|
||||
</Button>
|
||||
</VStack>
|
||||
@@ -1084,14 +1203,62 @@ export default function SubscriptionContent() {
|
||||
<Text color={secondaryText}>计费周期:</Text>
|
||||
<Text>{selectedCycle === 'monthly' ? '按月付费' : '按年付费'}</Text>
|
||||
</Flex>
|
||||
<Divider />
|
||||
<Flex justify="space-between" align="baseline">
|
||||
<Text color={secondaryText}>应付金额:</Text>
|
||||
<Text fontSize="2xl" fontWeight="bold" color="blue.500">
|
||||
¥{getCurrentPrice(selectedPlan).toFixed(2)}
|
||||
|
||||
{/* 价格明细 */}
|
||||
<Divider my={2} />
|
||||
|
||||
{priceInfo && priceInfo.is_upgrade && (
|
||||
<Box bg="blue.50" p={3} borderRadius="md" mb={2}>
|
||||
<HStack spacing={2} mb={2}>
|
||||
<Icon as={FaCheck} color="blue.500" boxSize={4} />
|
||||
<Text fontSize="sm" fontWeight="bold" color="blue.700">
|
||||
{priceInfo.upgrade_type === 'plan_upgrade' ? '套餐升级' :
|
||||
priceInfo.upgrade_type === 'cycle_change' ? '周期变更' : '套餐和周期调整'}
|
||||
</Text>
|
||||
</HStack>
|
||||
<VStack spacing={1} align="stretch" fontSize="xs">
|
||||
<Flex justify="space-between" color="gray.600">
|
||||
<Text>当前订阅: {priceInfo.current_plan === 'pro' ? 'Pro版' : 'Max版'} ({priceInfo.current_cycle === 'monthly' ? '月付' : '年付'})</Text>
|
||||
</Flex>
|
||||
<Flex justify="space-between" color="gray.600">
|
||||
<Text>剩余价值:</Text>
|
||||
<Text>¥{priceInfo.remaining_value.toFixed(2)}</Text>
|
||||
</Flex>
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Flex justify="space-between">
|
||||
<Text color={secondaryText}>
|
||||
{priceInfo && priceInfo.is_upgrade ? '新套餐价格:' : '套餐价格:'}
|
||||
</Text>
|
||||
<Text fontWeight="medium">
|
||||
¥{priceInfo ? priceInfo.new_plan_price.toFixed(2) : getCurrentPrice(selectedPlan).toFixed(2)}
|
||||
</Text>
|
||||
</Flex>
|
||||
{getSavingsText(selectedPlan) && (
|
||||
|
||||
{priceInfo && priceInfo.is_upgrade && priceInfo.remaining_value > 0 && (
|
||||
<Flex justify="space-between" color="blue.600">
|
||||
<Text>已付剩余抵扣:</Text>
|
||||
<Text>-¥{priceInfo.remaining_value.toFixed(2)}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{priceInfo && priceInfo.discount_amount > 0 && (
|
||||
<Flex justify="space-between" color="green.600">
|
||||
<Text>优惠码折扣:</Text>
|
||||
<Text>-¥{priceInfo.discount_amount.toFixed(2)}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
<Flex justify="space-between" align="baseline">
|
||||
<Text fontSize="lg" fontWeight="bold">实付金额:</Text>
|
||||
<Text fontSize="2xl" fontWeight="bold" color="blue.500">
|
||||
¥{priceInfo ? priceInfo.final_amount.toFixed(2) : getCurrentPrice(selectedPlan).toFixed(2)}
|
||||
</Text>
|
||||
</Flex>
|
||||
{getSavingsText(selectedPlan) && !priceInfo?.is_upgrade && (
|
||||
<Badge colorScheme="green" alignSelf="flex-end" fontSize="xs">
|
||||
{getSavingsText(selectedPlan)}
|
||||
</Badge>
|
||||
@@ -1104,6 +1271,53 @@ export default function SubscriptionContent() {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 优惠码输入 */}
|
||||
{selectedPlan && (
|
||||
<Box>
|
||||
<HStack spacing={2}>
|
||||
<Input
|
||||
placeholder="输入优惠码(可选)"
|
||||
value={promoCode}
|
||||
onChange={(e) => {
|
||||
setPromoCode(e.target.value.toUpperCase());
|
||||
setPromoCodeError('');
|
||||
}}
|
||||
size="md"
|
||||
isDisabled={promoCodeApplied}
|
||||
/>
|
||||
<Button
|
||||
colorScheme="purple"
|
||||
onClick={handleValidatePromoCode}
|
||||
isLoading={validatingPromo}
|
||||
isDisabled={!promoCode || promoCodeApplied}
|
||||
minW="80px"
|
||||
>
|
||||
应用
|
||||
</Button>
|
||||
</HStack>
|
||||
{promoCodeError && (
|
||||
<Text color="red.500" fontSize="sm" mt={2}>
|
||||
{promoCodeError}
|
||||
</Text>
|
||||
)}
|
||||
{promoCodeApplied && priceInfo && (
|
||||
<HStack mt={2} p={2} bg="green.50" borderRadius="md">
|
||||
<Icon as={FaCheck} color="green.500" />
|
||||
<Text color="green.700" fontSize="sm" fontWeight="medium" flex={1}>
|
||||
优惠码已应用!节省 ¥{priceInfo.discount_amount.toFixed(2)}
|
||||
</Text>
|
||||
<Icon
|
||||
as={FaTimes}
|
||||
color="gray.500"
|
||||
cursor="pointer"
|
||||
onClick={handleRemovePromoCode}
|
||||
_hover={{ color: 'red.500' }}
|
||||
/>
|
||||
</HStack>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Button
|
||||
colorScheme="green"
|
||||
size="lg"
|
||||
|
||||
Reference in New Issue
Block a user