feat: 添加合规

This commit is contained in:
zdl
2025-10-20 21:25:33 +08:00
parent d695f8ff7b
commit 6c96299b8f
42 changed files with 7118 additions and 289 deletions

View File

@@ -21,6 +21,14 @@ import {
Image,
Progress,
Divider,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
Heading,
Collapse,
} from '@chakra-ui/react';
import React, { useState, useEffect } from 'react';
import { logger } from '../../utils/logger';
@@ -35,6 +43,11 @@ import {
FaClock,
FaRedo,
FaCrown,
FaStar,
FaTimes,
FaInfinity,
FaChevronDown,
FaChevronUp,
} from 'react-icons/fa';
export default function SubscriptionContent() {
@@ -61,6 +74,7 @@ export default function SubscriptionContent() {
const [checkingPayment, setCheckingPayment] = useState(false);
const [autoCheckInterval, setAutoCheckInterval] = useState(null);
const [forceUpdating, setForceUpdating] = useState(false);
const [openFaqIndex, setOpenFaqIndex] = useState(null);
// 加载订阅套餐数据
useEffect(() => {
@@ -420,8 +434,34 @@ export default function SubscriptionContent() {
return `年付节省 ${percentage}%`;
};
// 统一的功能列表定义 - 基于商业定价(10月15日)文档
const allFeatures = [
// 新闻催化分析模块
{ name: '新闻信息流', free: true, pro: true, max: true },
{ name: '历史事件对比', free: 'TOP3', pro: true, max: true },
{ name: '事件传导链分析(AI)', free: '有限体验', pro: true, max: true },
{ name: '事件-相关标的分析', free: false, pro: true, max: true },
{ name: '相关概念展示', free: false, pro: true, max: true },
{ name: '板块深度分析(AI)', free: false, pro: false, max: true },
// 个股中心模块
{ name: 'AI复盘功能', free: true, pro: true, max: true },
{ name: '企业概览', free: '限制预览', pro: true, max: true },
{ name: '个股深度分析(AI)', free: '10家/月', pro: '50家/月', max: true },
{ name: '高效数据筛选工具', free: false, pro: true, max: true },
// 概念中心模块
{ name: '概念中心(548大概念)', free: 'TOP5', pro: true, max: true },
{ name: '历史时间轴查询', free: false, pro: '100天', max: true },
{ name: '概念高频更新', free: false, pro: false, max: true },
// 涨停分析模块
{ name: '涨停板块数据分析', free: true, pro: true, max: true },
{ name: '个股涨停分析', free: true, pro: true, max: true },
];
return (
<VStack spacing={6} align="stretch" w="100%">
<VStack spacing={6} align="stretch" w="100%" py={{base: 4, md: 6}}>
{/* 当前订阅状态 */}
{user && (
<Box
@@ -432,23 +472,12 @@ export default function SubscriptionContent() {
borderColor={borderColor}
shadow="sm"
>
<Flex justify="space-between" align="center" mb={4}>
<Text fontSize="lg" fontWeight="bold" color={textColor}>
当前订阅状态
</Text>
<Button
size="sm"
leftIcon={<Icon as={FaRedo} />}
onClick={handleRefreshUserStatus}
variant="ghost"
colorScheme="blue"
>
刷新
</Button>
</Flex>
<Flex align="center" justify="space-between">
<Box>
<HStack spacing={2} mb={2}>
<Flex align="center" justify="space-between" flexWrap="wrap" gap={3}>
{/* 左侧:当前订阅状态标签 */}
<HStack spacing={3}>
<Text fontSize="md" fontWeight="bold" color={textColor}>
当前订阅:
</Text>
<Badge
colorScheme={
user.subscription_type === 'max' ? 'purple' :
@@ -460,33 +489,36 @@ export default function SubscriptionContent() {
borderRadius="full"
fontSize="sm"
>
1SubscriptionContent {user.subscription_type}
{user.subscription_type === 'free' ? '基础版' :
user.subscription_type === 'pro' ? 'Pro 专业版' : 'Max 旗舰版'}
</Badge>
<Badge
colorScheme={user.subscription_status === 'active' ? 'green' : 'red'}
variant="subtle"
px={3}
py={1}
borderRadius="full"
>
{user.subscription_status === 'active' ? '已激活' : '未激活'}
</Badge>
</HStack>
<Badge
colorScheme={user.subscription_status === 'active' ? 'green' : 'red'}
variant="subtle"
px={3}
py={1}
borderRadius="full"
fontSize="sm"
>
{user.subscription_status === 'active' ? '已激活' : '未激活'}
</Badge>
</HStack>
{/* 右侧:到期时间和图标 */}
<HStack spacing={4}>
{user.subscription_end_date && (
<Text fontSize="sm" color={secondaryText}>
到期时间: {new Date(user.subscription_end_date).toLocaleDateString('zh-CN')}
</Text>
)}
</Box>
{user.subscription_status === 'active' && user.subscription_type !== 'free' && (
<Icon
as={user.subscription_type === 'max' ? FaCrown : FaGem}
color={user.subscription_type === 'max' ? 'purple.400' : 'blue.400'}
boxSize={8}
/>
)}
{user.subscription_status === 'active' && user.subscription_type !== 'free' && (
<Icon
as={user.subscription_type === 'max' ? FaCrown : FaGem}
color={user.subscription_type === 'max' ? 'purple.400' : 'blue.400'}
boxSize={6}
/>
)}
</HStack>
</Flex>
</Box>
)}
@@ -525,15 +557,119 @@ export default function SubscriptionContent() {
{/* 订阅套餐 */}
<Grid
templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }}
templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }}
gap={6}
>
{subscriptionPlans.length === 0 ? (
<Box gridColumn={{ base: '1', md: '1 / -1' }} textAlign="center" py={8}>
<Box gridColumn={{ base: '1', md: '1 / -1', lg: '1 / -1' }} textAlign="center" py={8}>
<Text color={secondaryText}>正在加载订阅套餐...</Text>
</Box>
) : (
subscriptionPlans.filter(plan => plan && plan.name).map((plan) => (
<>
{/* 免费版套餐 */}
<Box
position="relative"
borderRadius="2xl"
overflow="hidden"
border="2px solid"
borderColor="gray.300"
bg={bgCard}
transition="all 0.3s ease"
_hover={{
transform: 'translateY(-4px)',
shadow: 'xl',
}}
>
<VStack
spacing={4}
align="stretch"
p={6}
>
{/* 套餐头部 - 图标与标题同行 */}
<VStack spacing={2} align="stretch">
<Flex justify="space-between" align="center">
<HStack spacing={3}>
<Icon
as={FaStar}
boxSize={8}
color="gray.400"
/>
<Text fontSize="xl" fontWeight="bold" color={textColor}>
基础版
</Text>
</HStack>
<Badge
colorScheme="gray"
variant="outline"
px={4}
py={1}
borderRadius="full"
fontSize="md"
fontWeight="bold"
>
免费
</Badge>
</Flex>
<Text fontSize="xs" color={secondaryText} pl={11}>
免费体验核心功能,7项实用工具
</Text>
</VStack>
<Divider />
{/* 功能列表 */}
<VStack spacing={3} align="stretch" minH="200px">
{allFeatures.map((feature, index) => {
const hasFreeAccess = feature.free === true || typeof feature.free === 'string';
const freeLimit = typeof feature.free === 'string' ? feature.free : null;
return (
<HStack key={index} spacing={3} align="start">
<Icon
as={hasFreeAccess ? FaCheck : FaTimes}
color={hasFreeAccess ? 'blue.500' : 'gray.300'}
boxSize={4}
mt={0.5}
/>
<Text
fontSize="sm"
color={hasFreeAccess ? textColor : secondaryText}
flex={1}
>
{feature.name}
{freeLimit && (
<Text as="span" fontSize="xs" color="blue.500" ml={1}>
({freeLimit})
</Text>
)}
</Text>
</HStack>
);
})}
</VStack>
{/* 订阅按钮 */}
<Button
size="lg"
colorScheme="gray"
variant="solid"
isDisabled={true}
_hover={{
transform: 'scale(1.02)',
}}
transition="all 0.2s"
>
{user?.subscription_type === 'free' &&
user?.subscription_status === 'active'
? '✓ 当前套餐'
: '免费使用'
}
</Button>
</VStack>
</Box>
{/* 付费套餐 */}
{subscriptionPlans.filter(plan => plan && plan.name).map((plan) => (
<Box
key={plan.id}
position="relative"
@@ -558,6 +694,7 @@ export default function SubscriptionContent() {
bg="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
py={1}
textAlign="center"
zIndex={1}
>
<Text color="white" fontSize="xs" fontWeight="bold">
🔥 最受欢迎
@@ -566,61 +703,77 @@ export default function SubscriptionContent() {
)}
<VStack
spacing={5}
spacing={4}
align="stretch"
p={6}
pt={plan.name === 'max' ? 10 : 6}
>
{/* 套餐头部 */}
<VStack spacing={2} align="center">
<Icon
as={plan.name === 'pro' ? FaGem : FaCrown}
boxSize={12}
color={plan.name === 'pro' ? 'blue.400' : 'purple.400'}
/>
<Text fontSize="2xl" fontWeight="bold" color={textColor}>
{plan.display_name}
</Text>
<Text fontSize="sm" color={secondaryText} textAlign="center" minH="40px">
{plan.description}
</Text>
</VStack>
{/* 价格 */}
<VStack spacing={2}>
<HStack justify="center" align="baseline" spacing={1}>
<Text fontSize="sm" color={secondaryText}>¥</Text>
<Text fontSize="4xl" fontWeight="bold" color={textColor}>
{getCurrentPrice(plan).toFixed(0)}
{/* 套餐头部 - 图标与标题同行 */}
<VStack spacing={2} align="stretch">
<Flex justify="space-between" align="center">
<HStack spacing={3}>
<Icon
as={plan.name === 'pro' ? FaGem : FaCrown}
boxSize={8}
color={plan.name === 'pro' ? 'blue.400' : 'purple.400'}
/>
<Text fontSize="xl" fontWeight="bold" color={textColor}>
{plan.display_name}
</Text>
</HStack>
<HStack spacing={0} align="baseline">
<Text fontSize="md" color={secondaryText}>¥</Text>
<Text fontSize="2xl" fontWeight="extrabold" color={plan.name === 'pro' ? 'blue.500' : 'purple.500'}>
{getCurrentPrice(plan).toFixed(0)}
</Text>
<Text fontSize="sm" color={secondaryText}>
/{selectedCycle === 'monthly' ? '' : ''}
</Text>
</HStack>
</Flex>
<Flex justify="space-between" align="center">
<Text fontSize="xs" color={secondaryText} pl={11}>
{plan.description}
</Text>
<Text fontSize="sm" color={secondaryText}>
/ {selectedCycle === 'monthly' ? '' : ''}
</Text>
</HStack>
{getSavingsText(plan) && (
<Badge colorScheme="green" fontSize="xs" px={2} py={1}>
{getSavingsText(plan)}
</Badge>
)}
{getSavingsText(plan) && (
<Badge colorScheme="green" fontSize="xs" px={2} py={1}>
{getSavingsText(plan)}
</Badge>
)}
</Flex>
</VStack>
<Divider />
{/* 功能列表 */}
<VStack spacing={3} align="stretch" minH="200px">
{plan.features.map((feature, index) => (
<HStack key={index} spacing={3} align="start">
<Icon
as={FaCheck}
color={plan.name === 'max' ? 'purple.400' : 'blue.400'}
boxSize={4}
mt={0.5}
/>
<Text fontSize="sm" color={textColor} flex={1}>
{feature}
</Text>
</HStack>
))}
{allFeatures.map((feature, index) => {
const featureValue = feature[plan.name];
const isSupported = featureValue === true || typeof featureValue === 'string';
const limitText = typeof featureValue === 'string' ? featureValue : null;
return (
<HStack key={index} spacing={3} align="start">
<Icon
as={isSupported ? FaCheck : FaTimes}
color={isSupported ? 'blue.500' : 'gray.300'}
boxSize={4}
mt={0.5}
/>
<Text
fontSize="sm"
color={isSupported ? textColor : secondaryText}
flex={1}
>
{feature.name}
{limitText && (
<Text as="span" fontSize="xs" color={plan.name === 'pro' ? 'blue.500' : 'purple.500'} ml={1}>
({limitText})
</Text>
)}
</Text>
</HStack>
);
})}
</VStack>
{/* 订阅按钮 */}
@@ -646,9 +799,197 @@ export default function SubscriptionContent() {
</Button>
</VStack>
</Box>
)))}
))}
</>
)}
</Grid>
{/* FAQ 常见问题 */}
<Box
mt={12}
p={6}
borderRadius="xl"
bg={bgCard}
border="1px solid"
borderColor={borderColor}
shadow="sm"
>
<Heading size="lg" mb={6} textAlign="center" color={textColor}>
常见问题
</Heading>
<VStack spacing={4} align="stretch">
{/* FAQ 1 */}
<Box
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
overflow="hidden"
>
<Flex
p={4}
cursor="pointer"
onClick={() => setOpenFaqIndex(openFaqIndex === 0 ? null : 0)}
bg={openFaqIndex === 0 ? bgAccent : 'transparent'}
_hover={{ bg: bgAccent }}
transition="all 0.2s"
justify="space-between"
align="center"
>
<Text fontWeight="semibold" color={textColor}>
如何取消订阅
</Text>
<Icon
as={openFaqIndex === 0 ? FaChevronUp : FaChevronDown}
color={textColor}
/>
</Flex>
<Collapse in={openFaqIndex === 0}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
您可以随时在账户设置中取消订阅取消后您的订阅将在当前计费周期结束时到期期间您仍可继续使用付费功能取消后不会立即扣款也不会自动续费
</Text>
</Box>
</Collapse>
</Box>
{/* FAQ 2 */}
<Box
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
overflow="hidden"
>
<Flex
p={4}
cursor="pointer"
onClick={() => setOpenFaqIndex(openFaqIndex === 1 ? null : 1)}
bg={openFaqIndex === 1 ? bgAccent : 'transparent'}
_hover={{ bg: bgAccent }}
transition="all 0.2s"
justify="space-between"
align="center"
>
<Text fontWeight="semibold" color={textColor}>
支持哪些支付方式
</Text>
<Icon
as={openFaqIndex === 1 ? FaChevronUp : FaChevronDown}
color={textColor}
/>
</Flex>
<Collapse in={openFaqIndex === 1}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
我们目前支持微信支付扫描支付二维码后系统会自动检测支付状态并激活您的订阅支付过程安全可靠所有交易都经过加密处理
</Text>
</Box>
</Collapse>
</Box>
{/* FAQ 3 */}
<Box
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
overflow="hidden"
>
<Flex
p={4}
cursor="pointer"
onClick={() => setOpenFaqIndex(openFaqIndex === 2 ? null : 2)}
bg={openFaqIndex === 2 ? bgAccent : 'transparent'}
_hover={{ bg: bgAccent }}
transition="all 0.2s"
justify="space-between"
align="center"
>
<Text fontWeight="semibold" color={textColor}>
可以在月付和年付之间切换吗
</Text>
<Icon
as={openFaqIndex === 2 ? FaChevronUp : FaChevronDown}
color={textColor}
/>
</Flex>
<Collapse in={openFaqIndex === 2}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
可以您可以随时更改计费周期如果从月付切换到年付系统会计算剩余价值并应用到新的订阅中年付用户可享受20%的折扣优惠
</Text>
</Box>
</Collapse>
</Box>
{/* FAQ 4 */}
<Box
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
overflow="hidden"
>
<Flex
p={4}
cursor="pointer"
onClick={() => setOpenFaqIndex(openFaqIndex === 3 ? null : 3)}
bg={openFaqIndex === 3 ? bgAccent : 'transparent'}
_hover={{ bg: bgAccent }}
transition="all 0.2s"
justify="space-between"
align="center"
>
<Text fontWeight="semibold" color={textColor}>
是否提供退款
</Text>
<Icon
as={openFaqIndex === 3 ? FaChevronUp : FaChevronDown}
color={textColor}
/>
</Flex>
<Collapse in={openFaqIndex === 3}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
我们提供7天无理由退款保证如果您在订阅后7天内对服务不满意可以申请全额退款超过7天后我们将根据实际使用情况进行评估
</Text>
</Box>
</Collapse>
</Box>
{/* FAQ 5 */}
<Box
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
overflow="hidden"
>
<Flex
p={4}
cursor="pointer"
onClick={() => setOpenFaqIndex(openFaqIndex === 4 ? null : 4)}
bg={openFaqIndex === 4 ? bgAccent : 'transparent'}
_hover={{ bg: bgAccent }}
transition="all 0.2s"
justify="space-between"
align="center"
>
<Text fontWeight="semibold" color={textColor}>
Pro版和Max版有什么区别
</Text>
<Icon
as={openFaqIndex === 4 ? FaChevronUp : FaChevronDown}
color={textColor}
/>
</Flex>
<Collapse in={openFaqIndex === 4}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
Pro版适合个人专业用户提供高级图表历史数据分析等功能Max版则是为团队和企业设计额外提供实时数据推送API访问无限制的数据存储和团队协作功能并享有优先技术支持
</Text>
</Box>
</Collapse>
</Box>
</VStack>
</Box>
{/* 支付模态框 */}
{isPaymentModalOpen && (
<Modal