feat: 添加合规
This commit is contained in:
@@ -116,6 +116,8 @@ const CitationMark = ({ citationId, citation }) => {
|
||||
overlayInnerStyle={{ maxWidth: 340, padding: '8px' }}
|
||||
open={popoverVisible}
|
||||
onOpenChange={setPopoverVisible}
|
||||
zIndex={2000}
|
||||
getPopupContainer={(trigger) => trigger.parentElement || document.body}
|
||||
>
|
||||
<sup
|
||||
style={{
|
||||
|
||||
@@ -45,46 +45,63 @@ const CitedContent = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
// 判断是否显示标题栏(内联模式:title为空且不显示AI徽章)
|
||||
const showHeader = title || showAIBadge;
|
||||
|
||||
// 根据是否显示标题栏决定容器样式
|
||||
const defaultContainerStyle = showHeader ? {
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: 6,
|
||||
padding: 16
|
||||
} : {};
|
||||
|
||||
// 检查是否为内联模式
|
||||
const isInlineMode = containerStyle?.display && containerStyle.display.includes('inline');
|
||||
|
||||
// 根据内联模式选择容器元素类型
|
||||
const ContainerTag = isInlineMode ? 'span' : 'div';
|
||||
const ContentTag = isInlineMode ? 'span' : 'div';
|
||||
|
||||
return (
|
||||
<div
|
||||
<ContainerTag
|
||||
style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: 6,
|
||||
padding: 16,
|
||||
...defaultContainerStyle,
|
||||
...containerStyle
|
||||
}}
|
||||
>
|
||||
{/* 标题栏 */}
|
||||
<Space
|
||||
style={{
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<FileSearchOutlined style={{ color: '#1890ff', fontSize: 16 }} />
|
||||
<Text strong style={{ fontSize: 14 }}>
|
||||
{title}
|
||||
</Text>
|
||||
{/* 标题栏 - 仅在需要时显示 */}
|
||||
{showHeader && (
|
||||
<Space
|
||||
style={{
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<FileSearchOutlined style={{ color: '#1890ff', fontSize: 16 }} />
|
||||
<Text strong style={{ fontSize: 14 }}>
|
||||
{title}
|
||||
</Text>
|
||||
</Space>
|
||||
{showAIBadge && (
|
||||
<Tag
|
||||
icon={<RobotOutlined />}
|
||||
color="purple"
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
AI 生成
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
{showAIBadge && (
|
||||
<Tag
|
||||
icon={<RobotOutlined />}
|
||||
color="purple"
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
AI 生成
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
|
||||
{/* 带引用的文本内容 */}
|
||||
<div style={{ lineHeight: 1.8 }}>
|
||||
<ContentTag style={{ lineHeight: isInlineMode ? 'inherit' : 1.8 }}>
|
||||
{processed.segments.map((segment, index) => (
|
||||
<React.Fragment key={`segment-${segment.citationId}`}>
|
||||
{/* 文本片段 */}
|
||||
<Text style={{ fontSize: 14 }}>
|
||||
<Text style={{ fontSize: 14, display: 'inline' }}>
|
||||
{segment.text}
|
||||
</Text>
|
||||
|
||||
@@ -96,12 +113,12 @@ const CitedContent = ({
|
||||
|
||||
{/* 在片段之间添加逗号分隔符(最后一个不加) */}
|
||||
{index < processed.segments.length - 1 && (
|
||||
<Text style={{ fontSize: 14 }}>,</Text>
|
||||
<Text style={{ fontSize: 14, display: 'inline' }}>,</Text>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ContentTag>
|
||||
</ContainerTag>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -33,16 +33,18 @@ import {
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { ChevronDownIcon, HamburgerIcon, SunIcon, MoonIcon } from '@chakra-ui/icons';
|
||||
import { FiStar, FiCalendar } from 'react-icons/fi';
|
||||
import { FiStar, FiCalendar, FiUser, FiSettings, FiHome, FiLogOut } from 'react-icons/fi';
|
||||
import { FaCrown } from 'react-icons/fa';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||
import { logger } from '../../utils/logger';
|
||||
import SubscriptionBadge from '../Subscription/SubscriptionBadge';
|
||||
import { getApiBase } from '../../utils/apiConfig';
|
||||
import SubscriptionButton from '../Subscription/SubscriptionButton';
|
||||
import SubscriptionModal from '../Subscription/SubscriptionModal';
|
||||
|
||||
/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */
|
||||
const SecondaryNav = () => {
|
||||
const SecondaryNav = ({ showCompletenessAlert }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const navbarBg = useColorModeValue('gray.50', 'gray.700');
|
||||
@@ -107,7 +109,7 @@ const SecondaryNav = () => {
|
||||
borderColor={useColorModeValue('gray.200', 'gray.600')}
|
||||
py={2}
|
||||
position="sticky"
|
||||
top="60px"
|
||||
top={showCompletenessAlert ? "120px" : "60px"}
|
||||
zIndex={100}
|
||||
>
|
||||
<Container maxW="container.xl" px={4}>
|
||||
@@ -352,9 +354,6 @@ const NavItems = ({ isAuthenticated, user }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 计算 API 基础地址(移到组件外部,避免每次 render 重新创建)
|
||||
const getApiBase = () => (process.env.NODE_ENV === 'production' ? '' : (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'));
|
||||
|
||||
export default function HomeNavbar() {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const navigate = useNavigate();
|
||||
@@ -762,50 +761,42 @@ export default function HomeNavbar() {
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
{/* 订阅状态徽章 - 仅登录用户可见 */}
|
||||
{isAuthenticated && user && (
|
||||
<>
|
||||
<SubscriptionBadge
|
||||
subscriptionInfo={subscriptionInfo}
|
||||
onClick={() => setIsSubscriptionModalOpen(true)}
|
||||
/>
|
||||
<SubscriptionModal
|
||||
isOpen={isSubscriptionModalOpen}
|
||||
onClose={() => setIsSubscriptionModalOpen(false)}
|
||||
subscriptionInfo={subscriptionInfo}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 显示加载状态 */}
|
||||
{isLoading ? (
|
||||
<HStack spacing={2}>
|
||||
<Spinner size="sm" />
|
||||
<Text fontSize="sm" color="gray.500">检查登录状态...</Text>
|
||||
</HStack>
|
||||
<Spinner size="sm" color="blue.500" />
|
||||
) : isAuthenticated && user ? (
|
||||
// 已登录状态 - 用户菜单 + 功能菜单排列
|
||||
<HStack spacing={3}>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
bg="gray.800"
|
||||
color="white"
|
||||
size="sm"
|
||||
borderRadius="full"
|
||||
_hover={{ bg: 'gray.700' }}
|
||||
leftIcon={
|
||||
<Avatar
|
||||
size="xs"
|
||||
name={getDisplayName()}
|
||||
src={user.avatar_url}
|
||||
bg="blue.500"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{getDisplayName()}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{/* 用户头像+订阅徽章组合 */}
|
||||
<HStack spacing={2} align="center">
|
||||
{/* 用户头像菜单 */}
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
icon={
|
||||
<Avatar
|
||||
size="sm"
|
||||
name={getDisplayName()}
|
||||
src={user.avatar_url}
|
||||
bg="blue.500"
|
||||
border={subscriptionInfo.type !== 'free' ? '2px solid' : 'none'}
|
||||
borderColor={
|
||||
subscriptionInfo.type === 'max'
|
||||
? 'purple.500'
|
||||
: subscriptionInfo.type === 'pro'
|
||||
? 'blue.500'
|
||||
: 'transparent'
|
||||
}
|
||||
/>
|
||||
}
|
||||
bg="transparent"
|
||||
_hover={{ bg: useColorModeValue('gray.100', 'gray.700') }}
|
||||
borderRadius="full"
|
||||
position="relative"
|
||||
aria-label="用户菜单"
|
||||
>
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<Box px={3} py={2} borderBottom="1px" borderColor="gray.200">
|
||||
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
||||
<Text fontSize="xs" color="gray.500">{user.email}</Text>
|
||||
@@ -816,24 +807,52 @@ export default function HomeNavbar() {
|
||||
<Badge size="sm" colorScheme="green" mt={1}>微信已绑定</Badge>
|
||||
)}
|
||||
</Box>
|
||||
<MenuItem onClick={() => navigate('/home/profile')}>
|
||||
👤 个人资料
|
||||
{/* 账户管理组 */}
|
||||
<MenuItem icon={<FiUser />} onClick={() => navigate('/home/profile')}>
|
||||
个人资料
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => navigate('/home/pages/account/subscription')}>
|
||||
💎 订阅管理
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => navigate('/home/settings')}>
|
||||
⚙️ 账户设置
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => navigate('/home/center')}>
|
||||
🏠 个人中心
|
||||
<MenuItem icon={<FiSettings />} onClick={() => navigate('/home/settings')}>
|
||||
账户设置
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
<MenuItem onClick={handleLogout} color="red.500">
|
||||
🚪 退出登录
|
||||
{/* 功能入口组 */}
|
||||
<MenuItem icon={<FaCrown />} onClick={() => navigate('/home/pages/account/subscription')}>
|
||||
订阅管理
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
{/* 退出 */}
|
||||
<MenuItem icon={<FiLogOut />} onClick={handleLogout} color="red.500">
|
||||
退出登录
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Menu>
|
||||
|
||||
{/* 订阅徽章按钮 - 点击打开订阅弹窗 */}
|
||||
<SubscriptionButton
|
||||
subscriptionInfo={subscriptionInfo}
|
||||
onClick={() => setIsSubscriptionModalOpen(true)}
|
||||
/>
|
||||
|
||||
{/* 订阅管理弹窗 - 只在打开时渲染 */}
|
||||
{isSubscriptionModalOpen && (
|
||||
<SubscriptionModal
|
||||
isOpen={isSubscriptionModalOpen}
|
||||
onClose={() => setIsSubscriptionModalOpen(false)}
|
||||
subscriptionInfo={subscriptionInfo}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{/* 个人中心快捷按钮 */}
|
||||
<IconButton
|
||||
icon={<FiHome />}
|
||||
size="sm"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
onClick={() => navigate('/home/center')}
|
||||
aria-label="个人中心"
|
||||
_hover={{ bg: 'gray.700' }}
|
||||
/>
|
||||
|
||||
{/* 自选股 - 头像右侧 */}
|
||||
<Menu onOpen={loadWatchlistQuotes}>
|
||||
@@ -1261,7 +1280,7 @@ export default function HomeNavbar() {
|
||||
</Box>
|
||||
|
||||
{/* 二级导航栏 - 显示当前页面所属的二级菜单 */}
|
||||
{!isMobile && <SecondaryNav />}
|
||||
{!isMobile && <SecondaryNav showCompletenessAlert={showCompletenessAlert} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
209
src/components/Subscription/SubscriptionButton.js
Normal file
209
src/components/Subscription/SubscriptionButton.js
Normal file
@@ -0,0 +1,209 @@
|
||||
// src/components/Subscription/SubscriptionButton.js
|
||||
import React from 'react';
|
||||
import { Box, VStack, HStack, Text, Tooltip, Divider, useColorModeValue } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* 订阅徽章按钮组件 - 用于导航栏头像旁边
|
||||
* 简洁显示订阅等级,hover 显示详细卡片式 Tooltip
|
||||
*/
|
||||
export default function SubscriptionButton({ subscriptionInfo, onClick }) {
|
||||
const tooltipBg = useColorModeValue('white', 'gray.800');
|
||||
const tooltipBorder = useColorModeValue('gray.200', 'gray.600');
|
||||
const tooltipText = useColorModeValue('gray.700', 'gray.100');
|
||||
const dividerColor = useColorModeValue('gray.200', 'gray.600');
|
||||
|
||||
// 根据订阅类型返回样式配置
|
||||
const getButtonStyles = () => {
|
||||
if (subscriptionInfo.type === 'max') {
|
||||
return {
|
||||
bg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
icon: '👑',
|
||||
label: 'Max',
|
||||
shadow: '0 4px 12px rgba(118, 75, 162, 0.4)',
|
||||
hoverShadow: '0 6px 16px rgba(118, 75, 162, 0.5)',
|
||||
border: 'none',
|
||||
accentColor: '#764ba2',
|
||||
};
|
||||
}
|
||||
if (subscriptionInfo.type === 'pro') {
|
||||
return {
|
||||
bg: 'linear-gradient(135deg, #667eea 0%, #3182CE 100%)',
|
||||
color: 'white',
|
||||
icon: '💎',
|
||||
label: 'Pro',
|
||||
shadow: '0 4px 12px rgba(49, 130, 206, 0.4)',
|
||||
hoverShadow: '0 6px 16px rgba(49, 130, 206, 0.5)',
|
||||
border: 'none',
|
||||
accentColor: '#3182CE',
|
||||
};
|
||||
}
|
||||
// 基础版
|
||||
return {
|
||||
bg: 'transparent',
|
||||
color: useColorModeValue('gray.600', 'gray.400'),
|
||||
icon: '✨',
|
||||
label: '基础版',
|
||||
shadow: 'none',
|
||||
hoverShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
border: '1.5px solid',
|
||||
borderColor: useColorModeValue('gray.300', 'gray.600'),
|
||||
accentColor: useColorModeValue('#718096', '#A0AEC0'),
|
||||
};
|
||||
};
|
||||
|
||||
const styles = getButtonStyles();
|
||||
|
||||
// 增强的卡片式 Tooltip 内容
|
||||
const TooltipContent = () => {
|
||||
const { type, days_left, is_active } = subscriptionInfo;
|
||||
|
||||
// 基础版用户
|
||||
if (type === 'free') {
|
||||
return (
|
||||
<VStack spacing={2} align="stretch" minW="200px">
|
||||
<Text fontSize="md" fontWeight="bold" color={tooltipText}>
|
||||
✨ 基础版用户
|
||||
</Text>
|
||||
<Divider borderColor={dividerColor} />
|
||||
<Text fontSize="sm" color={tooltipText} opacity={0.8}>
|
||||
解锁更多高级功能
|
||||
</Text>
|
||||
<Box
|
||||
mt={1}
|
||||
px={3}
|
||||
py={2}
|
||||
borderRadius="md"
|
||||
bg="linear-gradient(135deg, #667eea 0%, #3182CE 100%)"
|
||||
color="white"
|
||||
textAlign="center"
|
||||
fontWeight="600"
|
||||
fontSize="sm"
|
||||
cursor="pointer"
|
||||
_hover={{ transform: 'scale(1.02)' }}
|
||||
transition="transform 0.2s"
|
||||
>
|
||||
🚀 立即升级
|
||||
</Box>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
// 付费用户
|
||||
const isExpired = !is_active;
|
||||
const isUrgent = days_left < 7;
|
||||
const isWarning = days_left < 30;
|
||||
|
||||
return (
|
||||
<VStack spacing={2} align="stretch" minW="200px">
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="md" fontWeight="bold" color={tooltipText}>
|
||||
{type === 'pro' ? '💎 Pro 会员' : '👑 Max 会员'}
|
||||
</Text>
|
||||
{isExpired && <Text fontSize="xs" color="red.500">已过期</Text>}
|
||||
</HStack>
|
||||
|
||||
<Divider borderColor={dividerColor} />
|
||||
|
||||
{/* 状态信息 */}
|
||||
{isExpired ? (
|
||||
<HStack spacing={2}>
|
||||
<Text fontSize="sm" color="red.500">❌</Text>
|
||||
<Text fontSize="sm" color={tooltipText}>
|
||||
会员已过期,续费恢复权益
|
||||
</Text>
|
||||
</HStack>
|
||||
) : (
|
||||
<VStack spacing={1} align="stretch">
|
||||
<HStack spacing={2}>
|
||||
<Text fontSize="sm" color={tooltipText}>
|
||||
{isUrgent ? '⚠️' : isWarning ? '⏰' : '📅'}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={tooltipText}>
|
||||
{isUrgent && <Text as="span" color="red.500" fontWeight="600">紧急!</Text>}
|
||||
{' '}还有 <Text as="span" fontWeight="600">{days_left}</Text> 天到期
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color={tooltipText} opacity={0.7} pl={6}>
|
||||
享受全部高级功能
|
||||
</Text>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* 行动按钮 */}
|
||||
<Box
|
||||
mt={1}
|
||||
px={3}
|
||||
py={2}
|
||||
borderRadius="md"
|
||||
bg={isExpired || isUrgent ? 'linear-gradient(135deg, #FC8181 0%, #F56565 100%)' : `linear-gradient(135deg, ${styles.accentColor} 0%, ${styles.accentColor}dd 100%)`}
|
||||
color="white"
|
||||
textAlign="center"
|
||||
fontWeight="600"
|
||||
fontSize="sm"
|
||||
cursor="pointer"
|
||||
_hover={{ transform: 'scale(1.02)' }}
|
||||
transition="transform 0.2s"
|
||||
>
|
||||
{isExpired ? '💳 立即续费' : isUrgent ? '⚡ 紧急续费' : '💼 管理订阅'}
|
||||
</Box>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={<TooltipContent />}
|
||||
hasArrow
|
||||
placement="bottom"
|
||||
bg={tooltipBg}
|
||||
color={tooltipText}
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={tooltipBorder}
|
||||
boxShadow="lg"
|
||||
p={3}
|
||||
>
|
||||
<Box
|
||||
as="button"
|
||||
onClick={onClick}
|
||||
px={3}
|
||||
py={2}
|
||||
minW="60px"
|
||||
h="40px"
|
||||
borderRadius="lg"
|
||||
bg={styles.bg}
|
||||
color={styles.color}
|
||||
border={styles.border}
|
||||
borderColor={styles.borderColor}
|
||||
cursor="pointer"
|
||||
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||
boxShadow={styles.shadow}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: styles.hoverShadow,
|
||||
}}
|
||||
_active={{
|
||||
transform: 'translateY(0)',
|
||||
}}
|
||||
>
|
||||
<Text fontSize="sm" fontWeight="600" lineHeight="1">
|
||||
{styles.icon} {styles.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
SubscriptionButton.propTypes = {
|
||||
subscriptionInfo: PropTypes.shape({
|
||||
type: PropTypes.oneOf(['free', 'pro', 'max']).isRequired,
|
||||
days_left: PropTypes.number,
|
||||
is_active: PropTypes.bool,
|
||||
}).isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user