Compare commits
7 Commits
74968d5bc8
...
c93f689954
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c93f689954 | ||
|
|
38499ce650 | ||
|
|
955e0db740 | ||
|
|
98653f042b | ||
|
|
eef383f56f | ||
|
|
d32cd616de | ||
|
|
31eb322ecc |
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,7 @@ import {
|
||||
useColorMode,
|
||||
useColorModeValue,
|
||||
useToast,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { ChevronDownIcon, HamburgerIcon, SunIcon, MoonIcon } from '@chakra-ui/icons';
|
||||
import { FiStar, FiCalendar, FiUser, FiSettings, FiHome, FiLogOut } from 'react-icons/fi';
|
||||
@@ -42,6 +43,7 @@ import { logger } from '../../utils/logger';
|
||||
import { getApiBase } from '../../utils/apiConfig';
|
||||
import SubscriptionButton from '../Subscription/SubscriptionButton';
|
||||
import SubscriptionModal from '../Subscription/SubscriptionModal';
|
||||
import { CrownIcon, TooltipContent } from '../Subscription/CrownTooltip';
|
||||
|
||||
/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */
|
||||
const SecondaryNav = ({ showCompletenessAlert }) => {
|
||||
@@ -184,6 +186,112 @@ const SecondaryNav = ({ showCompletenessAlert }) => {
|
||||
);
|
||||
};
|
||||
|
||||
/** 中屏"更多"菜单 - 用于平板和小笔记本 */
|
||||
const MoreNavMenu = ({ isAuthenticated, user }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// 辅助函数:判断导航项是否激活
|
||||
const isActive = useCallback((paths) => {
|
||||
return paths.some(path => location.pathname.includes(path));
|
||||
}, [location.pathname]);
|
||||
|
||||
if (!isAuthenticated || !user) return null;
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
variant="ghost"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
fontWeight="medium"
|
||||
>
|
||||
更多
|
||||
</MenuButton>
|
||||
<MenuList minW="300px" p={2}>
|
||||
{/* 高频跟踪组 */}
|
||||
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">高频跟踪</Text>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/community')}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/community') ? 'blue.50' : 'transparent'}
|
||||
>
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<Text fontSize="sm">新闻催化分析</Text>
|
||||
<HStack spacing={1}>
|
||||
<Badge size="sm" colorScheme="green">HOT</Badge>
|
||||
<Badge size="sm" colorScheme="red">NEW</Badge>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/concepts')}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/concepts') ? 'blue.50' : 'transparent'}
|
||||
>
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<Text fontSize="sm">概念中心</Text>
|
||||
<Badge size="sm" colorScheme="red">NEW</Badge>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
|
||||
<MenuDivider />
|
||||
|
||||
{/* 行情复盘组 */}
|
||||
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">行情复盘</Text>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/limit-analyse')}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/limit-analyse') ? 'blue.50' : 'transparent'}
|
||||
>
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<Text fontSize="sm">涨停分析</Text>
|
||||
<Badge size="sm" colorScheme="blue">FREE</Badge>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/stocks')}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/stocks') ? 'blue.50' : 'transparent'}
|
||||
>
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<Text fontSize="sm">个股中心</Text>
|
||||
<Badge size="sm" colorScheme="green">HOT</Badge>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/trading-simulation')}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/trading-simulation') ? 'blue.50' : 'transparent'}
|
||||
>
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<Text fontSize="sm">模拟盘</Text>
|
||||
<Badge size="sm" colorScheme="red">NEW</Badge>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
|
||||
<MenuDivider />
|
||||
|
||||
{/* AGENT社群组 */}
|
||||
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">AGENT社群</Text>
|
||||
<MenuItem isDisabled cursor="not-allowed" color="gray.400">
|
||||
<Text fontSize="sm" color="gray.400">今日热议</Text>
|
||||
</MenuItem>
|
||||
<MenuItem isDisabled cursor="not-allowed" color="gray.400">
|
||||
<Text fontSize="sm" color="gray.400">个股社区</Text>
|
||||
</MenuItem>
|
||||
|
||||
<MenuDivider />
|
||||
|
||||
{/* 联系我们 */}
|
||||
<MenuItem isDisabled cursor="not-allowed" color="gray.400">
|
||||
<Text fontSize="sm" color="gray.400">联系我们</Text>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
/** 桌面端导航 - 完全按照原网站
|
||||
* @TODO 添加逻辑 不展示导航case
|
||||
* 1.未登陆状态 && 是首页
|
||||
@@ -358,6 +466,8 @@ export default function HomeNavbar() {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
const isTablet = useBreakpointValue({ base: false, md: true, lg: false });
|
||||
const isDesktop = useBreakpointValue({ base: false, md: false, lg: true });
|
||||
const { user, isAuthenticated, logout, isLoading } = useAuth();
|
||||
const { openAuthModal } = useAuthModal();
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
@@ -669,44 +779,46 @@ export default function HomeNavbar() {
|
||||
<Box
|
||||
bg="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
|
||||
color="white"
|
||||
py={2}
|
||||
px={4}
|
||||
py={{ base: 2, md: 2 }}
|
||||
px={{ base: 2, md: 4 }}
|
||||
position="sticky"
|
||||
top={0}
|
||||
zIndex={1001}
|
||||
>
|
||||
<Container maxW="container.xl">
|
||||
<HStack justify="space-between" align="center">
|
||||
<HStack spacing={3}>
|
||||
<Icon as={FiStar} />
|
||||
<VStack spacing={0} align="start">
|
||||
<Text fontSize="sm" fontWeight="bold">
|
||||
<HStack justify="space-between" align="center" spacing={{ base: 2, md: 4 }}>
|
||||
<HStack spacing={{ base: 2, md: 3 }} flex={1} minW={0}>
|
||||
<Icon as={FiStar} display={{ base: 'none', sm: 'block' }} />
|
||||
<VStack spacing={0} align="start" flex={1} minW={0}>
|
||||
<Text fontSize={{ base: 'xs', md: 'sm' }} fontWeight="bold" noOfLines={1}>
|
||||
完善资料,享受更好服务
|
||||
</Text>
|
||||
<Text fontSize="xs" opacity={0.9}>
|
||||
<Text fontSize={{ base: '2xs', md: 'xs' }} opacity={0.9} noOfLines={1}>
|
||||
您还需要设置:{profileCompleteness.missingItems.join('、')}
|
||||
</Text>
|
||||
</VStack>
|
||||
<Text fontSize="xs" bg="whiteAlpha.300" px={2} py={1} borderRadius="full">
|
||||
<Text fontSize="2xs" bg="whiteAlpha.300" px={2} py={1} borderRadius="full" display={{ base: 'none', md: 'block' }}>
|
||||
{profileCompleteness.completenessPercentage}% 完成
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing={2}>
|
||||
<HStack spacing={{ base: 1, md: 2 }}>
|
||||
<Button
|
||||
size="sm"
|
||||
size={{ base: 'xs', md: 'sm' }}
|
||||
colorScheme="whiteAlpha"
|
||||
variant="ghost"
|
||||
onClick={() => navigate('/home/settings')}
|
||||
minH={{ base: '32px', md: '40px' }}
|
||||
>
|
||||
立即完善
|
||||
</Button>
|
||||
<IconButton
|
||||
size="sm"
|
||||
size={{ base: 'xs', md: 'sm' }}
|
||||
variant="ghost"
|
||||
colorScheme="whiteAlpha"
|
||||
icon={<Text>×</Text>}
|
||||
icon={<Text fontSize={{ base: 'xl', md: '2xl' }}>×</Text>}
|
||||
onClick={() => setShowCompletenessAlert(false)}
|
||||
aria-label="关闭提醒"
|
||||
minW={{ base: '32px', md: '40px' }}
|
||||
/>
|
||||
</HStack>
|
||||
</HStack>
|
||||
@@ -722,43 +834,53 @@ export default function HomeNavbar() {
|
||||
backdropFilter="blur(10px)"
|
||||
borderBottom="1px"
|
||||
borderColor={navbarBorder}
|
||||
py={3}
|
||||
py={{ base: 2, md: 3 }}
|
||||
>
|
||||
<Container maxW="container.xl" px={4}>
|
||||
<Container maxW="container.xl" px={{ base: 3, md: 4 }}>
|
||||
<Flex justify="space-between" align="center">
|
||||
{/* Logo - 价小前投研 */}
|
||||
<HStack spacing={6}>
|
||||
<HStack spacing={{ base: 3, md: 6 }}>
|
||||
<Text
|
||||
fontSize="xl"
|
||||
fontSize={{ base: 'lg', md: 'xl' }}
|
||||
fontWeight="bold"
|
||||
color={brandText}
|
||||
cursor="pointer"
|
||||
_hover={{ color: brandHover }}
|
||||
onClick={() => navigate('/home')}
|
||||
style={{ minWidth: '140px' }}
|
||||
style={{ minWidth: isMobile ? '100px' : '140px' }}
|
||||
noOfLines={1}
|
||||
>
|
||||
价小前投研
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 移动端菜单按钮 */}
|
||||
{/* 中间导航区域 - 响应式 */}
|
||||
{isMobile ? (
|
||||
// 移动端:汉堡菜单
|
||||
<IconButton
|
||||
icon={<HamburgerIcon />}
|
||||
variant="ghost"
|
||||
onClick={onOpen}
|
||||
aria-label="Open menu"
|
||||
/>
|
||||
) : <NavItems isAuthenticated={isAuthenticated} user={user} />}
|
||||
) : isTablet ? (
|
||||
// 中屏(平板):"更多"下拉菜单
|
||||
<MoreNavMenu isAuthenticated={isAuthenticated} user={user} />
|
||||
) : (
|
||||
// 大屏(桌面):完整导航菜单
|
||||
<NavItems isAuthenticated={isAuthenticated} user={user} />
|
||||
)}
|
||||
|
||||
{/* 右侧:日夜模式切换 + 登录/用户区 */}
|
||||
<HStack spacing={4}>
|
||||
<HStack spacing={{ base: 2, md: 4 }}>
|
||||
<IconButton
|
||||
aria-label="切换主题"
|
||||
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
minW={{ base: '36px', md: '40px' }}
|
||||
minH={{ base: '36px', md: '40px' }}
|
||||
/>
|
||||
|
||||
{/* 显示加载状态 */}
|
||||
@@ -766,95 +888,9 @@ export default function HomeNavbar() {
|
||||
<Spinner size="sm" color="blue.500" />
|
||||
) : isAuthenticated && user ? (
|
||||
// 已登录状态 - 用户菜单 + 功能菜单排列
|
||||
<HStack spacing={3}>
|
||||
{/* 用户头像+订阅徽章组合 */}
|
||||
<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>
|
||||
{user.phone && (
|
||||
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
||||
)}
|
||||
{user.has_wechat && (
|
||||
<Badge size="sm" colorScheme="green" mt={1}>微信已绑定</Badge>
|
||||
)}
|
||||
</Box>
|
||||
{/* 账户管理组 */}
|
||||
<MenuItem icon={<FiUser />} onClick={() => navigate('/home/profile')}>
|
||||
个人资料
|
||||
</MenuItem>
|
||||
<MenuItem icon={<FiSettings />} onClick={() => navigate('/home/settings')}>
|
||||
账户设置
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
{/* 功能入口组 */}
|
||||
<MenuItem icon={<FaCrown />} onClick={() => navigate('/home/pages/account/subscription')}>
|
||||
订阅管理
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
{/* 退出 */}
|
||||
<MenuItem icon={<FiLogOut />} onClick={handleLogout} color="red.500">
|
||||
退出登录
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</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' }}
|
||||
/>
|
||||
|
||||
{/* 自选股 - 头像右侧 */}
|
||||
<HStack spacing={{ base: 2, md: 3 }}>
|
||||
{/* 自选股 - 仅大屏显示 */}
|
||||
{isDesktop && (
|
||||
<Menu onOpen={loadWatchlistQuotes}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
@@ -929,8 +965,10 @@ export default function HomeNavbar() {
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
{/* 关注的事件 - 头像右侧 */}
|
||||
{/* 关注的事件 - 仅大屏显示 */}
|
||||
{isDesktop && (
|
||||
<Menu onOpen={loadFollowingEvents}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
@@ -1011,6 +1049,203 @@ export default function HomeNavbar() {
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
{/* 头像区域 - 响应式 */}
|
||||
{isDesktop ? (
|
||||
// 大屏:头像点击打开订阅弹窗
|
||||
<>
|
||||
<Tooltip
|
||||
label={<TooltipContent subscriptionInfo={subscriptionInfo} />}
|
||||
placement="bottom"
|
||||
hasArrow
|
||||
bg={useColorModeValue('white', 'gray.800')}
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={useColorModeValue('gray.200', 'gray.600')}
|
||||
boxShadow="lg"
|
||||
p={3}
|
||||
>
|
||||
<Box
|
||||
position="relative"
|
||||
cursor="pointer"
|
||||
onClick={() => setIsSubscriptionModalOpen(true)}
|
||||
>
|
||||
<CrownIcon subscriptionInfo={subscriptionInfo} />
|
||||
<Avatar
|
||||
size="sm"
|
||||
name={getDisplayName()}
|
||||
src={user.avatar_url}
|
||||
bg="blue.500"
|
||||
border={subscriptionInfo.type !== 'free' ? '2.5px solid' : 'none'}
|
||||
borderColor={
|
||||
subscriptionInfo.type === 'max' ? '#667eea' :
|
||||
subscriptionInfo.type === 'pro' ? '#667eea' : 'transparent'
|
||||
}
|
||||
_hover={{
|
||||
transform: 'scale(1.05)',
|
||||
boxShadow: subscriptionInfo.type !== 'free'
|
||||
? '0 4px 12px rgba(102, 126, 234, 0.4)'
|
||||
: 'md',
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{isSubscriptionModalOpen && (
|
||||
<SubscriptionModal
|
||||
isOpen={isSubscriptionModalOpen}
|
||||
onClose={() => setIsSubscriptionModalOpen(false)}
|
||||
subscriptionInfo={subscriptionInfo}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 中屏:头像作为下拉菜单,包含所有功能
|
||||
<Menu>
|
||||
<MenuButton>
|
||||
<Box position="relative">
|
||||
<CrownIcon subscriptionInfo={subscriptionInfo} />
|
||||
<Avatar
|
||||
size="sm"
|
||||
name={getDisplayName()}
|
||||
src={user.avatar_url}
|
||||
bg="blue.500"
|
||||
border={subscriptionInfo.type !== 'free' ? '2.5px solid' : 'none'}
|
||||
borderColor={
|
||||
subscriptionInfo.type === 'max' ? '#667eea' :
|
||||
subscriptionInfo.type === 'pro' ? '#667eea' : 'transparent'
|
||||
}
|
||||
_hover={{ transform: 'scale(1.05)' }}
|
||||
transition="all 0.2s"
|
||||
/>
|
||||
</Box>
|
||||
</MenuButton>
|
||||
<MenuList minW="320px">
|
||||
{/* 用户信息区 */}
|
||||
<Box px={3} py={2} borderBottom="1px" borderColor={useColorModeValue('gray.200', 'gray.600')}>
|
||||
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
||||
<Text fontSize="xs" color="gray.500">{user.email}</Text>
|
||||
{user.phone && (
|
||||
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
||||
)}
|
||||
{user.has_wechat && (
|
||||
<Badge size="sm" colorScheme="green" mt={1}>微信已绑定</Badge>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 订阅管理 */}
|
||||
<MenuItem icon={<FaCrown />} onClick={() => setIsSubscriptionModalOpen(true)}>
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<Text>订阅管理</Text>
|
||||
<Badge colorScheme={subscriptionInfo.type === 'free' ? 'gray' : 'purple'}>
|
||||
{subscriptionInfo.type === 'max' ? 'MAX' :
|
||||
subscriptionInfo.type === 'pro' ? 'PRO' : '免费版'}
|
||||
</Badge>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
|
||||
{isSubscriptionModalOpen && (
|
||||
<SubscriptionModal
|
||||
isOpen={isSubscriptionModalOpen}
|
||||
onClose={() => setIsSubscriptionModalOpen(false)}
|
||||
subscriptionInfo={subscriptionInfo}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MenuDivider />
|
||||
|
||||
{/* 自选股 */}
|
||||
<MenuItem icon={<FiStar />} onClick={() => navigate('/home/center')}>
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<Text>我的自选股</Text>
|
||||
{watchlistQuotes && watchlistQuotes.length > 0 && (
|
||||
<Badge>{watchlistQuotes.length}</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
|
||||
{/* 自选事件 */}
|
||||
<MenuItem icon={<FiCalendar />} onClick={() => navigate('/home/center')}>
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<Text>我的自选事件</Text>
|
||||
{followingEvents && followingEvents.length > 0 && (
|
||||
<Badge>{followingEvents.length}</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
|
||||
<MenuDivider />
|
||||
|
||||
{/* 个人中心 */}
|
||||
<MenuItem icon={<FiHome />} onClick={() => navigate('/home/center')}>
|
||||
个人中心
|
||||
</MenuItem>
|
||||
<MenuItem icon={<FiUser />} onClick={() => navigate('/home/profile')}>
|
||||
个人资料
|
||||
</MenuItem>
|
||||
<MenuItem icon={<FiSettings />} onClick={() => navigate('/home/settings')}>
|
||||
账户设置
|
||||
</MenuItem>
|
||||
|
||||
<MenuDivider />
|
||||
|
||||
{/* 退出登录 */}
|
||||
<MenuItem icon={<FiLogOut />} onClick={handleLogout} color="red.500">
|
||||
退出登录
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
{/* 个人中心下拉菜单 - 仅大屏显示 */}
|
||||
{isDesktop && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
_hover={{ bg: useColorModeValue('gray.100', 'gray.700') }}
|
||||
>
|
||||
个人中心
|
||||
</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>
|
||||
{user.phone && (
|
||||
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
||||
)}
|
||||
{user.has_wechat && (
|
||||
<Badge size="sm" colorScheme="green" mt={1}>微信已绑定</Badge>
|
||||
)}
|
||||
</Box>
|
||||
{/* 前往个人中心 */}
|
||||
<MenuItem icon={<FiHome />} onClick={() => navigate('/home/center')}>
|
||||
前往个人中心
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
{/* 账户管理组 */}
|
||||
<MenuItem icon={<FiUser />} onClick={() => navigate('/home/profile')}>
|
||||
个人资料
|
||||
</MenuItem>
|
||||
<MenuItem icon={<FiSettings />} onClick={() => navigate('/home/settings')}>
|
||||
账户设置
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
{/* 功能入口组 */}
|
||||
<MenuItem icon={<FaCrown />} onClick={() => navigate('/home/pages/account/subscription')}>
|
||||
订阅管理
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
{/* 退出 */}
|
||||
<MenuItem icon={<FiLogOut />} onClick={handleLogout} color="red.500">
|
||||
退出登录
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
</HStack>
|
||||
) : (
|
||||
// 未登录状态 - 单一按钮
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// src/components/NotificationContainer/index.js
|
||||
/**
|
||||
* 通知容器组件 - 右下角层叠显示实时通知
|
||||
* 金融资讯通知容器组件 - 右下角层叠显示实时通知
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
@@ -11,56 +12,69 @@ import {
|
||||
Text,
|
||||
IconButton,
|
||||
Icon,
|
||||
Badge,
|
||||
Button,
|
||||
useColorModeValue,
|
||||
Slide,
|
||||
ScaleFade,
|
||||
} from '@chakra-ui/react';
|
||||
import { MdClose, MdCheckCircle, MdError, MdWarning, MdInfo } from 'react-icons/md';
|
||||
import { MdClose, MdOpenInNew, MdSchedule, MdExpandMore, MdExpandLess } from 'react-icons/md';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
|
||||
// 通知类型对应的图标和颜色
|
||||
const NOTIFICATION_STYLES = {
|
||||
success: {
|
||||
icon: MdCheckCircle,
|
||||
colorScheme: 'green',
|
||||
bg: 'green.50',
|
||||
borderColor: 'green.400',
|
||||
iconColor: 'green.500',
|
||||
},
|
||||
error: {
|
||||
icon: MdError,
|
||||
colorScheme: 'red',
|
||||
bg: 'red.50',
|
||||
borderColor: 'red.400',
|
||||
iconColor: 'red.500',
|
||||
},
|
||||
warning: {
|
||||
icon: MdWarning,
|
||||
colorScheme: 'orange',
|
||||
bg: 'orange.50',
|
||||
borderColor: 'orange.400',
|
||||
iconColor: 'orange.500',
|
||||
},
|
||||
info: {
|
||||
icon: MdInfo,
|
||||
colorScheme: 'blue',
|
||||
bg: 'blue.50',
|
||||
borderColor: 'blue.400',
|
||||
iconColor: 'blue.500',
|
||||
},
|
||||
};
|
||||
import {
|
||||
NOTIFICATION_TYPE_CONFIGS,
|
||||
NOTIFICATION_TYPES,
|
||||
PRIORITY_CONFIGS,
|
||||
NOTIFICATION_CONFIG,
|
||||
formatNotificationTime,
|
||||
} from '../../constants/notificationTypes';
|
||||
|
||||
/**
|
||||
* 单个通知项组件
|
||||
*/
|
||||
const NotificationItem = ({ notification, onClose, isNewest = false }) => {
|
||||
const { id, severity = 'info', title, message } = notification;
|
||||
const style = NOTIFICATION_STYLES[severity] || NOTIFICATION_STYLES.info;
|
||||
const navigate = useNavigate();
|
||||
const { id, type, priority, title, content, isAIGenerated, clickable, link, author, publishTime, pushTime, extra } = notification;
|
||||
|
||||
const bgColor = useColorModeValue(style.bg, `${style.colorScheme}.900`);
|
||||
const borderColor = useColorModeValue(style.borderColor, `${style.colorScheme}.500`);
|
||||
// 严格判断可点击性:只有 clickable=true 且 link 存在才可点击
|
||||
const isActuallyClickable = clickable && link;
|
||||
|
||||
// 判断是否为预测通知
|
||||
const isPrediction = extra?.isPrediction;
|
||||
|
||||
// 获取类型配置
|
||||
let typeConfig = NOTIFICATION_TYPE_CONFIGS[type] || NOTIFICATION_TYPE_CONFIGS[NOTIFICATION_TYPES.EVENT_ALERT];
|
||||
|
||||
// 股票动向需要根据涨跌动态配置
|
||||
if (type === NOTIFICATION_TYPES.STOCK_ALERT && extra?.priceChange) {
|
||||
const priceChange = extra.priceChange;
|
||||
typeConfig = {
|
||||
...typeConfig,
|
||||
icon: typeConfig.getIcon(priceChange),
|
||||
colorScheme: typeConfig.getColorScheme(priceChange),
|
||||
bg: typeConfig.getBg(priceChange),
|
||||
borderColor: typeConfig.getBorderColor(priceChange),
|
||||
iconColor: typeConfig.getIconColor(priceChange),
|
||||
hoverBg: typeConfig.getHoverBg(priceChange),
|
||||
};
|
||||
}
|
||||
|
||||
// 获取优先级配置
|
||||
const priorityConfig = PRIORITY_CONFIGS[priority] || PRIORITY_CONFIGS.normal;
|
||||
|
||||
const bgColor = useColorModeValue(typeConfig.bg, `${typeConfig.colorScheme}.900`);
|
||||
const borderColor = useColorModeValue(typeConfig.borderColor, `${typeConfig.colorScheme}.500`);
|
||||
const textColor = useColorModeValue('gray.800', 'white');
|
||||
const subTextColor = useColorModeValue('gray.600', 'gray.300');
|
||||
const metaTextColor = useColorModeValue('gray.500', 'gray.400');
|
||||
const hoverBg = typeConfig.hoverBg;
|
||||
const closeButtonHoverBg = useColorModeValue(`${typeConfig.colorScheme}.200`, `${typeConfig.colorScheme}.700`);
|
||||
|
||||
// 点击处理(只有真正可点击时才执行)
|
||||
const handleClick = () => {
|
||||
if (isActuallyClickable) {
|
||||
navigate(link);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScaleFade initialScale={0.9} in={true}>
|
||||
@@ -69,72 +83,160 @@ const NotificationItem = ({ notification, onClose, isNewest = false }) => {
|
||||
borderLeft="4px solid"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
boxShadow={isNewest ? '2xl' : 'lg'} // 最新消息更强的阴影
|
||||
boxShadow={isNewest ? '2xl' : 'lg'}
|
||||
p={4}
|
||||
minW="350px"
|
||||
maxW="450px"
|
||||
w="400px" // 统一宽度
|
||||
position="relative"
|
||||
_hover={{
|
||||
cursor={isActuallyClickable ? 'pointer' : 'default'} // 严格判断
|
||||
onClick={isActuallyClickable ? handleClick : undefined} // 严格判断
|
||||
_hover={isActuallyClickable ? {
|
||||
boxShadow: 'xl',
|
||||
transform: 'translateX(-4px)',
|
||||
}}
|
||||
transform: 'translateY(-2px)',
|
||||
bg: hoverBg,
|
||||
} : {}} // 不可点击时无 hover 效果
|
||||
transition="all 0.2s"
|
||||
// 最新消息添加微妙的高亮边框
|
||||
{...(isNewest && {
|
||||
borderRight: '1px solid',
|
||||
borderRightColor: borderColor,
|
||||
borderTop: '1px solid',
|
||||
borderTopColor: useColorModeValue(`${style.colorScheme}.100`, `${style.colorScheme}.700`),
|
||||
borderTopColor: useColorModeValue(`${typeConfig.colorScheme}.100`, `${typeConfig.colorScheme}.700`),
|
||||
})}
|
||||
>
|
||||
<HStack spacing={3} align="start">
|
||||
{/* 图标 */}
|
||||
{/* 头部区域:图标 + 标题 + 优先级 + AI标识 */}
|
||||
<HStack spacing={2} align="start" mb={2}>
|
||||
{/* 类型图标 */}
|
||||
<Icon
|
||||
as={style.icon}
|
||||
w={6}
|
||||
h={6}
|
||||
color={style.iconColor}
|
||||
as={typeConfig.icon}
|
||||
w={5}
|
||||
h={5}
|
||||
color={typeConfig.iconColor}
|
||||
mt={0.5}
|
||||
flexShrink={0}
|
||||
/>
|
||||
|
||||
{/* 内容 */}
|
||||
<VStack align="start" spacing={1} flex={1} mr={6}>
|
||||
<Text
|
||||
fontSize="md"
|
||||
fontWeight="bold"
|
||||
color={textColor}
|
||||
lineHeight="short"
|
||||
{/* 标题 */}
|
||||
<Text
|
||||
fontSize="sm"
|
||||
fontWeight="bold"
|
||||
color={textColor}
|
||||
lineHeight="short"
|
||||
flex={1}
|
||||
noOfLines={2}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{/* 优先级标签 */}
|
||||
{priorityConfig.show && (
|
||||
<Badge
|
||||
colorScheme={priorityConfig.colorScheme}
|
||||
size="sm"
|
||||
flexShrink={0}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{message && (
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color={subTextColor}
|
||||
lineHeight="short"
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
{priorityConfig.label}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{/* 预测标识 */}
|
||||
{isPrediction && (
|
||||
<Badge
|
||||
colorScheme="gray"
|
||||
size="sm"
|
||||
flexShrink={0}
|
||||
>
|
||||
预测
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{/* AI 生成标识 */}
|
||||
{isAIGenerated && (
|
||||
<Badge
|
||||
colorScheme="purple"
|
||||
size="sm"
|
||||
flexShrink={0}
|
||||
>
|
||||
AI
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{/* 关闭按钮 */}
|
||||
<IconButton
|
||||
icon={<MdClose />}
|
||||
size="sm"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
colorScheme={style.colorScheme}
|
||||
colorScheme={typeConfig.colorScheme}
|
||||
aria-label="关闭通知"
|
||||
onClick={() => onClose(id)}
|
||||
position="absolute"
|
||||
top={2}
|
||||
right={2}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose(id);
|
||||
}}
|
||||
flexShrink={0}
|
||||
_hover={{
|
||||
bg: useColorModeValue(`${style.colorScheme}.100`, `${style.colorScheme}.800`),
|
||||
bg: closeButtonHoverBg,
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color={subTextColor}
|
||||
lineHeight="short"
|
||||
noOfLines={3}
|
||||
mb={3}
|
||||
pl={7} // 与图标对齐
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
|
||||
{/* 底部元数据区域 */}
|
||||
<HStack
|
||||
spacing={2}
|
||||
fontSize="xs"
|
||||
color={metaTextColor}
|
||||
pl={7} // 与图标对齐
|
||||
flexWrap="wrap"
|
||||
>
|
||||
{/* 作者信息(仅分析报告) */}
|
||||
{author && (
|
||||
<HStack spacing={1}>
|
||||
<Text>👤</Text>
|
||||
<Text>{author.name} - {author.organization}</Text>
|
||||
<Text>|</Text>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* 时间信息 */}
|
||||
<HStack spacing={1}>
|
||||
<Text>📅</Text>
|
||||
<Text>
|
||||
{publishTime && formatNotificationTime(publishTime)}
|
||||
{!publishTime && pushTime && formatNotificationTime(pushTime)}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 状态提示(仅预测通知) */}
|
||||
{extra?.statusHint && (
|
||||
<>
|
||||
<Text>|</Text>
|
||||
<HStack spacing={1} color="gray.400">
|
||||
<Icon as={MdSchedule} w={3} h={3} />
|
||||
<Text>{extra.statusHint}</Text>
|
||||
</HStack>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 可点击提示(仅真正可点击的通知) */}
|
||||
{isActuallyClickable && (
|
||||
<>
|
||||
<Text>|</Text>
|
||||
<HStack spacing={1}>
|
||||
<Icon as={MdOpenInNew} w={3} h={3} />
|
||||
<Text>查看详情</Text>
|
||||
</HStack>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
</ScaleFade>
|
||||
);
|
||||
@@ -145,12 +247,24 @@ const NotificationItem = ({ notification, onClose, isNewest = false }) => {
|
||||
*/
|
||||
const NotificationContainer = () => {
|
||||
const { notifications, removeNotification } = useNotification();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// 如果没有通知,不渲染
|
||||
if (notifications.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 根据展开状态决定显示的通知
|
||||
const maxVisible = NOTIFICATION_CONFIG.maxVisible;
|
||||
const hasMore = notifications.length > maxVisible;
|
||||
const visibleNotifications = isExpanded ? notifications : notifications.slice(0, maxVisible);
|
||||
const hiddenCount = notifications.length - maxVisible;
|
||||
|
||||
// 颜色配置
|
||||
const collapseBg = useColorModeValue('gray.100', 'gray.700');
|
||||
const collapseHoverBg = useColorModeValue('gray.200', 'gray.600');
|
||||
const collapseTextColor = useColorModeValue('gray.700', 'gray.200');
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="fixed"
|
||||
@@ -164,10 +278,10 @@ const NotificationContainer = () => {
|
||||
align="flex-end"
|
||||
pointerEvents="auto"
|
||||
>
|
||||
{notifications.map((notification, index) => (
|
||||
{visibleNotifications.map((notification, index) => (
|
||||
<Slide
|
||||
key={notification.id}
|
||||
direction="right"
|
||||
direction="bottom"
|
||||
in={true}
|
||||
style={{
|
||||
position: 'relative',
|
||||
@@ -181,6 +295,28 @@ const NotificationContainer = () => {
|
||||
/>
|
||||
</Slide>
|
||||
))}
|
||||
|
||||
{/* 折叠/展开按钮 */}
|
||||
{hasMore && (
|
||||
<ScaleFade initialScale={0.9} in={true}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="solid"
|
||||
bg={collapseBg}
|
||||
color={collapseTextColor}
|
||||
_hover={{ bg: collapseHoverBg }}
|
||||
leftIcon={<Icon as={isExpanded ? MdExpandLess : MdExpandMore} />}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
boxShadow="md"
|
||||
borderRadius="md"
|
||||
>
|
||||
{isExpanded
|
||||
? '收起通知'
|
||||
: NOTIFICATION_CONFIG.collapse.textTemplate.replace('{count}', hiddenCount)
|
||||
}
|
||||
</Button>
|
||||
</ScaleFade>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/components/NotificationTestTool/index.js
|
||||
/**
|
||||
* 通知测试工具 - 仅在开发环境显示
|
||||
* 用于手动测试通知功能
|
||||
* 金融资讯通知测试工具 - 仅在开发环境显示
|
||||
* 用于手动测试4种通知类型
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
@@ -15,87 +15,299 @@ import {
|
||||
Collapse,
|
||||
useDisclosure,
|
||||
Badge,
|
||||
Divider,
|
||||
} from '@chakra-ui/react';
|
||||
import { MdNotifications, MdClose, MdVolumeOff, MdVolumeUp } from 'react-icons/md';
|
||||
import { MdNotifications, MdClose, MdVolumeOff, MdVolumeUp, MdCampaign, MdTrendingUp, MdArticle, MdAssessment } from 'react-icons/md';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
import { SOCKET_TYPE } from '../../services/socket';
|
||||
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '../../constants/notificationTypes';
|
||||
|
||||
const NotificationTestTool = () => {
|
||||
const { isOpen, onToggle } = useDisclosure();
|
||||
const { addNotification, soundEnabled, toggleSound, isConnected, clearAllNotifications, notifications } = useNotification();
|
||||
const { addNotification, soundEnabled, toggleSound, isConnected, clearAllNotifications, notifications, browserPermission, requestBrowserPermission } = useNotification();
|
||||
const [testCount, setTestCount] = useState(0);
|
||||
|
||||
// 浏览器权限状态标签
|
||||
const getPermissionLabel = () => {
|
||||
switch (browserPermission) {
|
||||
case 'granted':
|
||||
return '已授权';
|
||||
case 'denied':
|
||||
return '已拒绝';
|
||||
case 'default':
|
||||
return '未授权';
|
||||
default:
|
||||
return '不支持';
|
||||
}
|
||||
};
|
||||
|
||||
const getPermissionColor = () => {
|
||||
switch (browserPermission) {
|
||||
case 'granted':
|
||||
return 'green';
|
||||
case 'denied':
|
||||
return 'red';
|
||||
case 'default':
|
||||
return 'gray';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
// 请求浏览器权限
|
||||
const handleRequestPermission = async () => {
|
||||
await requestBrowserPermission();
|
||||
};
|
||||
|
||||
// 只在开发环境显示
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const testNotifications = [
|
||||
{
|
||||
severity: 'success',
|
||||
title: '买入成功',
|
||||
message: '您的订单已成功执行:买入 贵州茅台(600519) 100股',
|
||||
},
|
||||
{
|
||||
severity: 'error',
|
||||
title: '委托失败',
|
||||
message: '卖出订单失败:资金不足',
|
||||
},
|
||||
{
|
||||
severity: 'warning',
|
||||
title: '价格预警',
|
||||
message: '您关注的股票已触达预设价格',
|
||||
},
|
||||
{
|
||||
severity: 'info',
|
||||
title: '持仓提醒',
|
||||
message: '您持有的股票今日涨幅达 5.2%',
|
||||
},
|
||||
];
|
||||
|
||||
const handleTestNotification = (index) => {
|
||||
const notif = testNotifications[index];
|
||||
// 公告通知测试数据
|
||||
const testAnnouncement = () => {
|
||||
addNotification({
|
||||
...notif,
|
||||
type: 'trade_alert',
|
||||
autoClose: 8000,
|
||||
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
|
||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
||||
title: '【测试】贵州茅台发布2024年度财报公告',
|
||||
content: '2024年度营收同比增长15.2%,净利润创历史新高,董事会建议每10股派息180元',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/event-detail/test001',
|
||||
extra: {
|
||||
announcementType: '财报',
|
||||
companyCode: '600519',
|
||||
companyName: '贵州茅台',
|
||||
},
|
||||
autoClose: 10000,
|
||||
});
|
||||
setTestCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
const handleMultipleNotifications = () => {
|
||||
testNotifications.forEach((notif, index) => {
|
||||
setTimeout(() => {
|
||||
addNotification({
|
||||
...notif,
|
||||
type: 'trade_alert',
|
||||
autoClose: 10000,
|
||||
});
|
||||
}, index * 600);
|
||||
// 股票动向测试数据(涨)
|
||||
const testStockAlertUp = () => {
|
||||
addNotification({
|
||||
type: NOTIFICATION_TYPES.STOCK_ALERT,
|
||||
priority: PRIORITY_LEVELS.URGENT,
|
||||
title: '【测试】您关注的股票触发预警',
|
||||
content: '宁德时代(300750) 当前价格 ¥245.50,盘中涨幅达 +5.2%,已触达您设置的目标价位',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/stock-overview?code=300750',
|
||||
extra: {
|
||||
stockCode: '300750',
|
||||
stockName: '宁德时代',
|
||||
priceChange: '+5.2%',
|
||||
currentPrice: '245.50',
|
||||
},
|
||||
autoClose: 10000,
|
||||
});
|
||||
setTestCount(prev => prev + testNotifications.length);
|
||||
setTestCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
const handleMaxLimitTest = () => {
|
||||
// 测试最大限制:快速发送6条,验证只保留最新5条
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
// 股票动向测试数据(跌)
|
||||
const testStockAlertDown = () => {
|
||||
addNotification({
|
||||
type: NOTIFICATION_TYPES.STOCK_ALERT,
|
||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
||||
title: '【测试】您关注的股票异常波动',
|
||||
content: '比亚迪(002594) 5分钟内跌幅达 -3.8%,当前价格 ¥198.20,建议关注',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/stock-overview?code=002594',
|
||||
extra: {
|
||||
stockCode: '002594',
|
||||
stockName: '比亚迪',
|
||||
priceChange: '-3.8%',
|
||||
currentPrice: '198.20',
|
||||
},
|
||||
autoClose: 10000,
|
||||
});
|
||||
setTestCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
// 事件动向测试数据
|
||||
const testEventAlert = () => {
|
||||
addNotification({
|
||||
type: NOTIFICATION_TYPES.EVENT_ALERT,
|
||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
||||
title: '【测试】央行宣布降准0.5个百分点',
|
||||
content: '中国人民银行宣布下调金融机构存款准备金率0.5个百分点,释放长期资金约1万亿元,利好股市',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/event-detail/test003',
|
||||
extra: {
|
||||
eventId: 'test003',
|
||||
relatedStocks: 12,
|
||||
impactLevel: '重大利好',
|
||||
},
|
||||
autoClose: 12000,
|
||||
});
|
||||
setTestCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
// 分析报告测试数据(非AI)
|
||||
const testAnalysisReport = () => {
|
||||
addNotification({
|
||||
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
|
||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
||||
title: '【测试】医药行业深度报告:创新药迎来政策拐点',
|
||||
content: 'CXO板块持续受益于全球创新药研发外包需求,建议关注药明康德、凯莱英等龙头企业',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
author: {
|
||||
name: '李明',
|
||||
organization: '中信证券',
|
||||
},
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/forecast-report?id=test004',
|
||||
extra: {
|
||||
reportType: '行业研报',
|
||||
industry: '医药',
|
||||
},
|
||||
autoClose: 12000,
|
||||
});
|
||||
setTestCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
// AI分析报告测试数据
|
||||
const testAIReport = () => {
|
||||
addNotification({
|
||||
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
|
||||
priority: PRIORITY_LEVELS.NORMAL,
|
||||
title: '【测试】AI产业链投资机会分析',
|
||||
content: '随着大模型应用加速落地,算力、数据、应用三大方向均存在投资机会,重点关注海光信息、寒武纪',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
author: {
|
||||
name: 'AI分析师',
|
||||
organization: '价值前沿',
|
||||
},
|
||||
isAIGenerated: true,
|
||||
clickable: true,
|
||||
link: '/forecast-report?id=test005',
|
||||
extra: {
|
||||
reportType: '策略报告',
|
||||
industry: '人工智能',
|
||||
},
|
||||
autoClose: 12000,
|
||||
});
|
||||
setTestCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
// 预测通知测试数据(不可跳转)
|
||||
const testPrediction = () => {
|
||||
addNotification({
|
||||
type: NOTIFICATION_TYPES.EVENT_ALERT,
|
||||
priority: PRIORITY_LEVELS.NORMAL,
|
||||
title: '【测试】【预测】央行可能宣布降准政策',
|
||||
content: '基于最新宏观数据分析,预计央行将在本周宣布降准0.5个百分点,释放长期资金',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: true,
|
||||
clickable: false, // ❌ 不可点击
|
||||
link: null,
|
||||
extra: {
|
||||
isPrediction: true,
|
||||
statusHint: '详细报告生成中...',
|
||||
},
|
||||
autoClose: 15000,
|
||||
});
|
||||
setTestCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
// 预测→详情流程测试(先推预测,5秒后推详情)
|
||||
const testPredictionFlow = () => {
|
||||
// 阶段 1: 推送预测
|
||||
addNotification({
|
||||
type: NOTIFICATION_TYPES.EVENT_ALERT,
|
||||
priority: PRIORITY_LEVELS.NORMAL,
|
||||
title: '【测试】【预测】新能源汽车补贴政策将延期',
|
||||
content: '根据政策趋势分析,预计财政部将宣布新能源汽车购置补贴政策延长至2025年底',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: true,
|
||||
clickable: false,
|
||||
link: null,
|
||||
extra: {
|
||||
isPrediction: true,
|
||||
statusHint: '详细报告生成中...',
|
||||
relatedPredictionId: 'pred_test_001',
|
||||
},
|
||||
autoClose: 15000,
|
||||
});
|
||||
setTestCount(prev => prev + 1);
|
||||
|
||||
// 阶段 2: 5秒后推送详情
|
||||
setTimeout(() => {
|
||||
addNotification({
|
||||
type: NOTIFICATION_TYPES.EVENT_ALERT,
|
||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
||||
title: '【测试】新能源汽车补贴政策延期至2025年底',
|
||||
content: '财政部宣布新能源汽车购置补贴政策延长至2025年底,涉及比亚迪、理想汽车等5家龙头企业',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true, // ✅ 可点击
|
||||
link: '/event-detail/test_pred_001',
|
||||
extra: {
|
||||
isPrediction: false,
|
||||
relatedPredictionId: 'pred_test_001',
|
||||
eventId: 'test_pred_001',
|
||||
relatedStocks: 5,
|
||||
impactLevel: '重大利好',
|
||||
},
|
||||
autoClose: 12000,
|
||||
});
|
||||
setTestCount(prev => prev + 1);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// 测试全部类型(层叠效果)
|
||||
const testAllTypes = () => {
|
||||
const tests = [testAnnouncement, testStockAlertUp, testEventAlert, testAnalysisReport];
|
||||
tests.forEach((test, index) => {
|
||||
setTimeout(() => test(), index * 600);
|
||||
});
|
||||
};
|
||||
|
||||
// 测试优先级
|
||||
const testPriority = () => {
|
||||
[
|
||||
{ priority: PRIORITY_LEVELS.URGENT, label: '紧急' },
|
||||
{ priority: PRIORITY_LEVELS.IMPORTANT, label: '重要' },
|
||||
{ priority: PRIORITY_LEVELS.NORMAL, label: '普通' },
|
||||
].forEach((item, index) => {
|
||||
setTimeout(() => {
|
||||
addNotification({
|
||||
severity: i % 2 === 0 ? 'success' : 'info',
|
||||
title: `测试消息 #${i}`,
|
||||
message: `这是第 ${i} 条测试消息(共6条,应只保留最新5条)`,
|
||||
type: 'trade_alert',
|
||||
autoClose: 12000,
|
||||
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
|
||||
priority: item.priority,
|
||||
title: `【测试】${item.label}优先级通知`,
|
||||
content: `这是一条${item.label}优先级的测试通知,用于验证优先级标签显示`,
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: false,
|
||||
autoClose: 10000,
|
||||
});
|
||||
}, i * 400);
|
||||
}
|
||||
setTestCount(prev => prev + 6);
|
||||
setTestCount(prev => prev + 1);
|
||||
}, index * 600);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="fixed"
|
||||
top={4}
|
||||
top="316px"
|
||||
right={4}
|
||||
zIndex={9998}
|
||||
bg="white"
|
||||
@@ -114,7 +326,7 @@ const NotificationTestTool = () => {
|
||||
>
|
||||
<MdNotifications size={20} />
|
||||
<Text fontSize="sm" fontWeight="bold">
|
||||
通知测试工具
|
||||
金融资讯测试工具
|
||||
</Text>
|
||||
<Badge colorScheme={isConnected ? 'green' : 'red'} ml="auto">
|
||||
{isConnected ? 'Connected' : 'Disconnected'}
|
||||
@@ -122,6 +334,9 @@ const NotificationTestTool = () => {
|
||||
<Badge colorScheme="purple">
|
||||
{SOCKET_TYPE}
|
||||
</Badge>
|
||||
<Badge colorScheme={getPermissionColor()}>
|
||||
浏览器: {getPermissionLabel()}
|
||||
</Badge>
|
||||
<IconButton
|
||||
icon={isOpen ? <MdClose /> : <MdNotifications />}
|
||||
size="xs"
|
||||
@@ -133,60 +348,151 @@ const NotificationTestTool = () => {
|
||||
|
||||
{/* 工具面板 */}
|
||||
<Collapse in={isOpen} animateOpacity>
|
||||
<VStack p={4} spacing={3} align="stretch" minW="250px">
|
||||
<Text fontSize="xs" color="gray.600">
|
||||
点击按钮测试不同类型的通知
|
||||
<VStack p={4} spacing={3} align="stretch" minW="280px">
|
||||
<Text fontSize="xs" color="gray.600" fontWeight="bold">
|
||||
通知类型测试
|
||||
</Text>
|
||||
|
||||
{/* 测试按钮 */}
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="green"
|
||||
onClick={() => handleTestNotification(0)}
|
||||
>
|
||||
成功通知
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
onClick={() => handleTestNotification(1)}
|
||||
>
|
||||
错误通知
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="orange"
|
||||
onClick={() => handleTestNotification(2)}
|
||||
>
|
||||
警告通知
|
||||
</Button>
|
||||
|
||||
{/* 公告通知 */}
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
onClick={() => handleTestNotification(3)}
|
||||
leftIcon={<MdCampaign />}
|
||||
onClick={testAnnouncement}
|
||||
>
|
||||
信息通知
|
||||
公告通知
|
||||
</Button>
|
||||
|
||||
{/* 股票动向 */}
|
||||
<HStack spacing={2}>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
leftIcon={<MdTrendingUp />}
|
||||
onClick={testStockAlertUp}
|
||||
flex={1}
|
||||
>
|
||||
股票上涨
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="green"
|
||||
leftIcon={<MdTrendingUp style={{ transform: 'rotate(180deg)' }} />}
|
||||
onClick={testStockAlertDown}
|
||||
flex={1}
|
||||
>
|
||||
股票下跌
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
{/* 事件动向 */}
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="purple"
|
||||
onClick={handleMultipleNotifications}
|
||||
colorScheme="orange"
|
||||
leftIcon={<MdArticle />}
|
||||
onClick={testEventAlert}
|
||||
>
|
||||
层叠通知(4条)
|
||||
事件动向
|
||||
</Button>
|
||||
|
||||
{/* 分析报告 */}
|
||||
<HStack spacing={2}>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="purple"
|
||||
leftIcon={<MdAssessment />}
|
||||
onClick={testAnalysisReport}
|
||||
flex={1}
|
||||
>
|
||||
分析报告
|
||||
</Button>
|
||||
<Badge colorScheme="purple" alignSelf="center">AI</Badge>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="purple"
|
||||
variant="outline"
|
||||
onClick={testAIReport}
|
||||
flex={1}
|
||||
>
|
||||
AI报告
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
{/* 预测通知 */}
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="gray"
|
||||
leftIcon={<MdArticle />}
|
||||
onClick={testPrediction}
|
||||
>
|
||||
预测通知(不可跳转)
|
||||
</Button>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Text fontSize="xs" color="gray.600" fontWeight="bold">
|
||||
组合测试
|
||||
</Text>
|
||||
|
||||
{/* 层叠测试 */}
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="teal"
|
||||
onClick={testAllTypes}
|
||||
>
|
||||
层叠测试(4种类型)
|
||||
</Button>
|
||||
|
||||
{/* 优先级测试 */}
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="pink"
|
||||
onClick={handleMaxLimitTest}
|
||||
onClick={testPriority}
|
||||
>
|
||||
测试最大限制(6条→5条)
|
||||
优先级测试(3个级别)
|
||||
</Button>
|
||||
|
||||
{/* 预测→详情流程测试 */}
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="cyan"
|
||||
onClick={testPredictionFlow}
|
||||
>
|
||||
预测→详情流程(5秒延迟)
|
||||
</Button>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Text fontSize="xs" color="gray.600" fontWeight="bold">
|
||||
浏览器通知
|
||||
</Text>
|
||||
|
||||
{/* 请求权限按钮 */}
|
||||
{browserPermission !== 'granted' && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme={browserPermission === 'denied' ? 'red' : 'blue'}
|
||||
onClick={handleRequestPermission}
|
||||
isDisabled={browserPermission === 'denied'}
|
||||
>
|
||||
{browserPermission === 'denied' ? '权限已拒绝' : '请求浏览器权限'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 浏览器通知状态说明 */}
|
||||
{browserPermission === 'granted' && (
|
||||
<Text fontSize="xs" color="green.500">
|
||||
✅ 浏览器通知已启用
|
||||
</Text>
|
||||
)}
|
||||
{browserPermission === 'denied' && (
|
||||
<Text fontSize="xs" color="red.500">
|
||||
❌ 请在浏览器设置中允许通知
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 功能按钮 */}
|
||||
<HStack spacing={2}>
|
||||
<Button
|
||||
|
||||
137
src/components/Subscription/CrownTooltip.js
Normal file
137
src/components/Subscription/CrownTooltip.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// src/components/Subscription/CrownTooltip.js
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Divider,
|
||||
useColorModeValue
|
||||
} from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Tooltip 内容组件 - 显示详细的会员信息
|
||||
* 导出此组件供头像也使用相同的 Tooltip 内容
|
||||
*/
|
||||
export const TooltipContent = ({ subscriptionInfo }) => {
|
||||
const tooltipText = useColorModeValue('gray.700', 'gray.100');
|
||||
const dividerColor = useColorModeValue('gray.200', 'gray.600');
|
||||
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>
|
||||
<Text fontSize="xs" color={tooltipText} opacity={0.6} textAlign="center" mt={1}>
|
||||
点击头像升级会员
|
||||
</Text>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
// 付费用户
|
||||
const isExpired = !is_active;
|
||||
const isUrgent = days_left < 7;
|
||||
const isWarning = days_left < 30;
|
||||
|
||||
return (
|
||||
<VStack spacing={2} align="stretch" minW="220px">
|
||||
<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">
|
||||
{isUrgent ? '⚠️' : isWarning ? '⏰' : '📅'}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={tooltipText}>
|
||||
{isUrgent && <Text as="span" color="red.500" fontWeight="600">紧急! </Text>}
|
||||
还有 <Text as="span" fontWeight="600" color={isUrgent ? 'red.500' : isWarning ? 'orange.500' : tooltipText}>{days_left}</Text> 天到期
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color={tooltipText} opacity={0.7} pl={6}>
|
||||
享受全部高级功能
|
||||
</Text>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* 操作提示 */}
|
||||
<Text fontSize="xs" color={tooltipText} opacity={0.6} textAlign="center" mt={1}>
|
||||
{isExpired || isUrgent ? '点击头像立即续费' : '点击头像管理订阅'}
|
||||
</Text>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 皇冠图标组件 - 显示在头像左上角的会员标识(纯图标,无 Tooltip)
|
||||
* Tooltip 由外层统一包裹
|
||||
*
|
||||
* @param {Object} subscriptionInfo - 订阅信息
|
||||
* @param {string} subscriptionInfo.type - 订阅类型: 'free' | 'pro' | 'max'
|
||||
*/
|
||||
export function CrownIcon({ subscriptionInfo }) {
|
||||
// 基础版用户不显示皇冠
|
||||
if (subscriptionInfo.type === 'free') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-8px"
|
||||
left="-8px"
|
||||
zIndex={2}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
w="24px"
|
||||
h="24px"
|
||||
_hover={{
|
||||
transform: 'scale(1.2)',
|
||||
}}
|
||||
transition="all 0.3s ease"
|
||||
>
|
||||
<Text fontSize="20px" filter="drop-shadow(0 2px 4px rgba(0,0,0,0.3))">
|
||||
{subscriptionInfo.type === 'max' ? '👑' : '💎'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
CrownIcon.propTypes = {
|
||||
subscriptionInfo: PropTypes.shape({
|
||||
type: PropTypes.oneOf(['free', 'pro', 'max']).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
TooltipContent.propTypes = {
|
||||
subscriptionInfo: PropTypes.shape({
|
||||
type: PropTypes.oneOf(['free', 'pro', 'max']).isRequired,
|
||||
days_left: PropTypes.number,
|
||||
is_active: PropTypes.bool,
|
||||
}).isRequired,
|
||||
};
|
||||
@@ -17,26 +17,28 @@ export default function SubscriptionButton({ subscriptionInfo, onClick }) {
|
||||
const getButtonStyles = () => {
|
||||
if (subscriptionInfo.type === 'max') {
|
||||
return {
|
||||
bg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
bg: 'transparent',
|
||||
color: '#3182CE',
|
||||
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',
|
||||
shadow: 'none',
|
||||
hoverShadow: '0 2px 8px rgba(49, 130, 206, 0.2)',
|
||||
border: '1.5px solid',
|
||||
borderColor: '#4299E1',
|
||||
accentColor: '#3182CE',
|
||||
};
|
||||
}
|
||||
if (subscriptionInfo.type === 'pro') {
|
||||
return {
|
||||
bg: 'linear-gradient(135deg, #667eea 0%, #3182CE 100%)',
|
||||
color: 'white',
|
||||
bg: 'transparent',
|
||||
color: '#667eea',
|
||||
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',
|
||||
shadow: 'none',
|
||||
hoverShadow: '0 2px 8px rgba(102, 126, 234, 0.2)',
|
||||
border: '1.5px solid',
|
||||
borderColor: '#667eea',
|
||||
accentColor: '#667eea',
|
||||
};
|
||||
}
|
||||
// 基础版
|
||||
@@ -168,11 +170,11 @@ export default function SubscriptionButton({ subscriptionInfo, onClick }) {
|
||||
<Box
|
||||
as="button"
|
||||
onClick={onClick}
|
||||
px={3}
|
||||
py={2}
|
||||
minW="60px"
|
||||
h="40px"
|
||||
borderRadius="lg"
|
||||
px={2}
|
||||
py={1}
|
||||
w="70px"
|
||||
h="32px"
|
||||
borderRadius="md"
|
||||
bg={styles.bg}
|
||||
color={styles.color}
|
||||
border={styles.border}
|
||||
@@ -184,7 +186,7 @@ export default function SubscriptionButton({ subscriptionInfo, onClick }) {
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
transform: 'translateY(-1px)',
|
||||
boxShadow: styles.hoverShadow,
|
||||
}}
|
||||
_active={{
|
||||
@@ -192,7 +194,7 @@ export default function SubscriptionButton({ subscriptionInfo, onClick }) {
|
||||
}}
|
||||
>
|
||||
<Text fontSize="sm" fontWeight="600" lineHeight="1">
|
||||
{styles.icon} {styles.label}
|
||||
<Text as="span" fontSize="md">{styles.icon}</Text> {styles.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
181
src/constants/notificationTypes.js
Normal file
181
src/constants/notificationTypes.js
Normal file
@@ -0,0 +1,181 @@
|
||||
// src/constants/notificationTypes.js
|
||||
/**
|
||||
* 金融资讯通知系统 - 类型定义和常量
|
||||
*/
|
||||
|
||||
import { MdCampaign, MdTrendingUp, MdTrendingDown, MdArticle, MdAssessment } from 'react-icons/md';
|
||||
|
||||
// 通知类型
|
||||
export const NOTIFICATION_TYPES = {
|
||||
ANNOUNCEMENT: 'announcement', // 公告通知
|
||||
STOCK_ALERT: 'stock_alert', // 股票动向
|
||||
EVENT_ALERT: 'event_alert', // 事件动向
|
||||
ANALYSIS_REPORT: 'analysis_report', // 分析报告
|
||||
};
|
||||
|
||||
// 优先级
|
||||
export const PRIORITY_LEVELS = {
|
||||
URGENT: 'urgent', // 紧急
|
||||
IMPORTANT: 'important', // 重要
|
||||
NORMAL: 'normal', // 普通
|
||||
};
|
||||
|
||||
// 通知状态(用于预测通知)
|
||||
export const NOTIFICATION_STATUS = {
|
||||
PREDICTION: 'prediction', // 预测状态(详情未就绪)
|
||||
READY: 'ready', // 详情已就绪
|
||||
};
|
||||
|
||||
// 通知系统配置
|
||||
export const NOTIFICATION_CONFIG = {
|
||||
// 显示策略
|
||||
maxVisible: 3, // 最多显示3条通知
|
||||
maxHistory: 15, // 历史保留15条(折叠区)
|
||||
|
||||
// 自动关闭时长(毫秒)- 按优先级区分
|
||||
autoCloseDuration: {
|
||||
[PRIORITY_LEVELS.URGENT]: 0, // 紧急:不自动关闭
|
||||
[PRIORITY_LEVELS.IMPORTANT]: 30000, // 重要:30秒
|
||||
[PRIORITY_LEVELS.NORMAL]: 15000, // 普通:15秒
|
||||
},
|
||||
|
||||
// 推送频率配置(测试模式)
|
||||
mockPush: {
|
||||
interval: 60000, // 60秒推送一次
|
||||
minBatch: 1, // 最少1条
|
||||
maxBatch: 2, // 最多2条
|
||||
},
|
||||
|
||||
// 折叠配置
|
||||
collapse: {
|
||||
threshold: 3, // 超过3条开始折叠
|
||||
textTemplate: '还有 {count} 条通知', // 折叠提示文案
|
||||
},
|
||||
};
|
||||
|
||||
// 优先级标签配置
|
||||
export const PRIORITY_CONFIGS = {
|
||||
[PRIORITY_LEVELS.URGENT]: {
|
||||
label: '紧急',
|
||||
colorScheme: 'red',
|
||||
show: true,
|
||||
},
|
||||
[PRIORITY_LEVELS.IMPORTANT]: {
|
||||
label: '重要',
|
||||
colorScheme: 'orange',
|
||||
show: true,
|
||||
},
|
||||
[PRIORITY_LEVELS.NORMAL]: {
|
||||
label: '',
|
||||
colorScheme: 'gray',
|
||||
show: false, // 普通优先级不显示标签
|
||||
},
|
||||
};
|
||||
|
||||
// 通知类型样式配置
|
||||
export const NOTIFICATION_TYPE_CONFIGS = {
|
||||
[NOTIFICATION_TYPES.ANNOUNCEMENT]: {
|
||||
name: '公告通知',
|
||||
icon: MdCampaign,
|
||||
colorScheme: 'blue',
|
||||
bg: 'blue.50',
|
||||
borderColor: 'blue.400',
|
||||
iconColor: 'blue.500',
|
||||
hoverBg: 'blue.100',
|
||||
},
|
||||
[NOTIFICATION_TYPES.STOCK_ALERT]: {
|
||||
name: '股票动向',
|
||||
// 图标根据涨跌动态设置
|
||||
getIcon: (priceChange) => {
|
||||
if (!priceChange) return MdTrendingUp;
|
||||
return priceChange.startsWith('+') ? MdTrendingUp : MdTrendingDown;
|
||||
},
|
||||
// 颜色根据涨跌动态设置
|
||||
getColorScheme: (priceChange) => {
|
||||
if (!priceChange) return 'red';
|
||||
return priceChange.startsWith('+') ? 'red' : 'green';
|
||||
},
|
||||
getBg: (priceChange) => {
|
||||
const scheme = NOTIFICATION_TYPE_CONFIGS[NOTIFICATION_TYPES.STOCK_ALERT].getColorScheme(priceChange);
|
||||
return `${scheme}.50`;
|
||||
},
|
||||
getBorderColor: (priceChange) => {
|
||||
const scheme = NOTIFICATION_TYPE_CONFIGS[NOTIFICATION_TYPES.STOCK_ALERT].getColorScheme(priceChange);
|
||||
return `${scheme}.400`;
|
||||
},
|
||||
getIconColor: (priceChange) => {
|
||||
const scheme = NOTIFICATION_TYPE_CONFIGS[NOTIFICATION_TYPES.STOCK_ALERT].getColorScheme(priceChange);
|
||||
return `${scheme}.500`;
|
||||
},
|
||||
getHoverBg: (priceChange) => {
|
||||
const scheme = NOTIFICATION_TYPE_CONFIGS[NOTIFICATION_TYPES.STOCK_ALERT].getColorScheme(priceChange);
|
||||
return `${scheme}.100`;
|
||||
},
|
||||
},
|
||||
[NOTIFICATION_TYPES.EVENT_ALERT]: {
|
||||
name: '事件动向',
|
||||
icon: MdArticle,
|
||||
colorScheme: 'orange',
|
||||
bg: 'orange.50',
|
||||
borderColor: 'orange.400',
|
||||
iconColor: 'orange.500',
|
||||
hoverBg: 'orange.100',
|
||||
},
|
||||
[NOTIFICATION_TYPES.ANALYSIS_REPORT]: {
|
||||
name: '分析报告',
|
||||
icon: MdAssessment,
|
||||
colorScheme: 'purple',
|
||||
bg: 'purple.50',
|
||||
borderColor: 'purple.400',
|
||||
iconColor: 'purple.500',
|
||||
hoverBg: 'purple.100',
|
||||
},
|
||||
};
|
||||
|
||||
// 时间格式化辅助函数
|
||||
export const formatNotificationTime = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
|
||||
// 小于1分钟
|
||||
if (diff < 60000) {
|
||||
return '刚刚';
|
||||
}
|
||||
|
||||
// 小于1小时
|
||||
if (diff < 3600000) {
|
||||
return `${Math.floor(diff / 60000)}分钟前`;
|
||||
}
|
||||
|
||||
// 小于24小时
|
||||
if (diff < 86400000) {
|
||||
return `${Math.floor(diff / 3600000)}小时前`;
|
||||
}
|
||||
|
||||
// 今天
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const dateDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
if (dateDay.getTime() === today.getTime()) {
|
||||
return `今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 昨天
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
if (dateDay.getTime() === yesterday.getTime()) {
|
||||
return `昨天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 其他
|
||||
return `${date.getMonth() + 1}-${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
NOTIFICATION_TYPES,
|
||||
PRIORITY_LEVELS,
|
||||
NOTIFICATION_STATUS,
|
||||
PRIORITY_CONFIGS,
|
||||
NOTIFICATION_TYPE_CONFIGS,
|
||||
formatNotificationTime,
|
||||
};
|
||||
@@ -7,6 +7,8 @@ import React, { createContext, useContext, useState, useEffect, useCallback, use
|
||||
import { logger } from '../utils/logger';
|
||||
import socket, { SOCKET_TYPE } from '../services/socket';
|
||||
import notificationSound from '../assets/sounds/notification.wav';
|
||||
import { browserNotificationService } from '../services/browserNotificationService';
|
||||
import { PRIORITY_LEVELS, NOTIFICATION_CONFIG } from '../constants/notificationTypes';
|
||||
|
||||
// 创建通知上下文
|
||||
const NotificationContext = createContext();
|
||||
@@ -25,6 +27,7 @@ export const NotificationProvider = ({ children }) => {
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [soundEnabled, setSoundEnabled] = useState(true);
|
||||
const [browserPermission, setBrowserPermission] = useState(browserNotificationService.getPermissionStatus());
|
||||
const audioRef = useRef(null);
|
||||
|
||||
// 初始化音频
|
||||
@@ -57,54 +60,6 @@ export const NotificationProvider = ({ children }) => {
|
||||
}
|
||||
}, [soundEnabled]);
|
||||
|
||||
/**
|
||||
* 添加通知到队列
|
||||
* @param {object} notification - 通知对象
|
||||
*/
|
||||
const addNotification = useCallback((notification) => {
|
||||
const newNotification = {
|
||||
id: notification.id || `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: notification.type || 'info',
|
||||
severity: notification.severity || 'info',
|
||||
title: notification.title || '通知',
|
||||
message: notification.message || '',
|
||||
timestamp: notification.timestamp || Date.now(),
|
||||
autoClose: notification.autoClose !== undefined ? notification.autoClose : 8000,
|
||||
...notification,
|
||||
};
|
||||
|
||||
logger.info('NotificationContext', 'Adding notification', newNotification);
|
||||
|
||||
// 新消息插入到数组开头,最多保留5条
|
||||
setNotifications(prev => {
|
||||
const updated = [newNotification, ...prev];
|
||||
const maxNotifications = 5;
|
||||
|
||||
// 如果超过最大数量,移除最旧的(数组末尾)
|
||||
if (updated.length > maxNotifications) {
|
||||
const removed = updated.slice(maxNotifications);
|
||||
removed.forEach(old => {
|
||||
logger.info('NotificationContext', 'Auto-removing old notification', { id: old.id });
|
||||
});
|
||||
return updated.slice(0, maxNotifications);
|
||||
}
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
// 播放音效
|
||||
playNotificationSound();
|
||||
|
||||
// 自动关闭
|
||||
if (newNotification.autoClose && newNotification.autoClose > 0) {
|
||||
setTimeout(() => {
|
||||
removeNotification(newNotification.id);
|
||||
}, newNotification.autoClose);
|
||||
}
|
||||
|
||||
return newNotification.id;
|
||||
}, [playNotificationSound]);
|
||||
|
||||
/**
|
||||
* 移除通知
|
||||
* @param {string} id - 通知ID
|
||||
@@ -133,6 +88,144 @@ export const NotificationProvider = ({ children }) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 请求浏览器通知权限
|
||||
*/
|
||||
const requestBrowserPermission = useCallback(async () => {
|
||||
logger.info('NotificationContext', 'Requesting browser notification permission');
|
||||
const permission = await browserNotificationService.requestPermission();
|
||||
setBrowserPermission(permission);
|
||||
return permission;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 发送浏览器通知
|
||||
*/
|
||||
const sendBrowserNotification = useCallback((notificationData) => {
|
||||
if (browserPermission !== 'granted') {
|
||||
logger.warn('NotificationContext', 'Browser permission not granted');
|
||||
return;
|
||||
}
|
||||
|
||||
const { priority, title, content, link, type } = notificationData;
|
||||
|
||||
// 生成唯一 tag
|
||||
const tag = `${type}_${Date.now()}`;
|
||||
|
||||
// 判断是否需要用户交互(紧急通知不自动关闭)
|
||||
const requireInteraction = priority === PRIORITY_LEVELS.URGENT;
|
||||
|
||||
// 发送浏览器通知
|
||||
const notification = browserNotificationService.sendNotification({
|
||||
title: title || '新通知',
|
||||
body: content || '',
|
||||
tag,
|
||||
requireInteraction,
|
||||
data: { link },
|
||||
autoClose: requireInteraction ? 0 : 8000,
|
||||
});
|
||||
|
||||
// 设置点击处理(聚焦窗口并跳转)
|
||||
if (notification && link) {
|
||||
notification.onclick = () => {
|
||||
window.focus();
|
||||
// 使用 window.location 跳转(不需要 React Router)
|
||||
window.location.hash = link;
|
||||
notification.close();
|
||||
};
|
||||
}
|
||||
|
||||
logger.info('NotificationContext', 'Browser notification sent', { title, tag });
|
||||
}, [browserPermission]);
|
||||
|
||||
/**
|
||||
* 添加网页通知(内部方法)
|
||||
*/
|
||||
const addWebNotification = useCallback((newNotification) => {
|
||||
// 新消息插入到数组开头,最多保留 maxHistory 条
|
||||
setNotifications(prev => {
|
||||
const updated = [newNotification, ...prev];
|
||||
const maxNotifications = NOTIFICATION_CONFIG.maxHistory;
|
||||
|
||||
// 如果超过最大数量,移除最旧的(数组末尾)
|
||||
if (updated.length > maxNotifications) {
|
||||
const removed = updated.slice(maxNotifications);
|
||||
removed.forEach(old => {
|
||||
logger.info('NotificationContext', 'Auto-removing old notification', { id: old.id });
|
||||
});
|
||||
return updated.slice(0, maxNotifications);
|
||||
}
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
// 播放音效
|
||||
playNotificationSound();
|
||||
|
||||
// 自动关闭
|
||||
if (newNotification.autoClose && newNotification.autoClose > 0) {
|
||||
setTimeout(() => {
|
||||
removeNotification(newNotification.id);
|
||||
}, newNotification.autoClose);
|
||||
}
|
||||
}, [playNotificationSound, removeNotification]);
|
||||
|
||||
/**
|
||||
* 添加通知到队列
|
||||
* @param {object} notification - 通知对象
|
||||
*/
|
||||
const addNotification = useCallback((notification) => {
|
||||
// 根据优先级获取自动关闭时长
|
||||
const priority = notification.priority || PRIORITY_LEVELS.NORMAL;
|
||||
const defaultAutoClose = NOTIFICATION_CONFIG.autoCloseDuration[priority];
|
||||
|
||||
const newNotification = {
|
||||
id: notification.id || `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: notification.type || 'info',
|
||||
severity: notification.severity || 'info',
|
||||
title: notification.title || '通知',
|
||||
message: notification.message || '',
|
||||
timestamp: notification.timestamp || Date.now(),
|
||||
priority: priority,
|
||||
autoClose: notification.autoClose !== undefined ? notification.autoClose : defaultAutoClose,
|
||||
...notification,
|
||||
};
|
||||
|
||||
logger.info('NotificationContext', 'Adding notification', newNotification);
|
||||
|
||||
const isPageHidden = document.hidden; // 页面是否在后台
|
||||
|
||||
// ========== 智能分发策略 ==========
|
||||
|
||||
// 策略 1: 紧急通知 - 双重保障(浏览器 + 网页)
|
||||
if (priority === PRIORITY_LEVELS.URGENT) {
|
||||
logger.info('NotificationContext', 'Urgent notification: sending browser + web');
|
||||
// 总是发送浏览器通知
|
||||
sendBrowserNotification(newNotification);
|
||||
// 如果在前台,也显示网页通知
|
||||
if (!isPageHidden) {
|
||||
addWebNotification(newNotification);
|
||||
}
|
||||
}
|
||||
// 策略 2: 重要通知 - 智能切换(后台=浏览器,前台=网页)
|
||||
else if (priority === PRIORITY_LEVELS.IMPORTANT) {
|
||||
if (isPageHidden) {
|
||||
logger.info('NotificationContext', 'Important notification (background): sending browser');
|
||||
sendBrowserNotification(newNotification);
|
||||
} else {
|
||||
logger.info('NotificationContext', 'Important notification (foreground): sending web');
|
||||
addWebNotification(newNotification);
|
||||
}
|
||||
}
|
||||
// 策略 3: 普通通知 - 仅网页通知
|
||||
else {
|
||||
logger.info('NotificationContext', 'Normal notification: sending web only');
|
||||
addWebNotification(newNotification);
|
||||
}
|
||||
|
||||
return newNotification.id;
|
||||
}, [sendBrowserNotification, addWebNotification]);
|
||||
|
||||
// 连接到 Socket 服务
|
||||
useEffect(() => {
|
||||
logger.info('NotificationContext', 'Initializing socket connection...');
|
||||
@@ -147,9 +240,10 @@ export const NotificationProvider = ({ children }) => {
|
||||
|
||||
// 如果使用 mock,可以启动定期推送
|
||||
if (SOCKET_TYPE === 'MOCK') {
|
||||
// 启动模拟推送:每20秒推送1-2条消息
|
||||
socket.startMockPush(20000, 2);
|
||||
logger.info('NotificationContext', 'Mock push started');
|
||||
// 启动模拟推送:使用配置的间隔和数量
|
||||
const { interval, maxBatch } = NOTIFICATION_CONFIG.mockPush;
|
||||
socket.startMockPush(interval, maxBatch);
|
||||
logger.info('NotificationContext', 'Mock push started', { interval, maxBatch });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -191,10 +285,12 @@ export const NotificationProvider = ({ children }) => {
|
||||
notifications,
|
||||
isConnected,
|
||||
soundEnabled,
|
||||
browserPermission,
|
||||
addNotification,
|
||||
removeNotification,
|
||||
clearAllNotifications,
|
||||
toggleSound,
|
||||
requestBrowserPermission,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
208
src/services/browserNotificationService.js
Normal file
208
src/services/browserNotificationService.js
Normal file
@@ -0,0 +1,208 @@
|
||||
// src/services/browserNotificationService.js
|
||||
/**
|
||||
* 浏览器原生通知服务
|
||||
* 提供系统级通知功能(Web Notifications API)
|
||||
*/
|
||||
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
class BrowserNotificationService {
|
||||
constructor() {
|
||||
this.permission = this.isSupported() ? Notification.permission : 'denied';
|
||||
this.activeNotifications = new Map(); // 存储活跃的通知
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查浏览器是否支持通知 API
|
||||
*/
|
||||
isSupported() {
|
||||
return 'Notification' in window;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前权限状态
|
||||
* @returns {string} 'granted' | 'denied' | 'default'
|
||||
*/
|
||||
getPermissionStatus() {
|
||||
if (!this.isSupported()) {
|
||||
return 'denied';
|
||||
}
|
||||
return Notification.permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求通知权限
|
||||
* @returns {Promise<string>} 权限状态
|
||||
*/
|
||||
async requestPermission() {
|
||||
if (!this.isSupported()) {
|
||||
logger.warn('browserNotificationService', 'Notifications not supported');
|
||||
return 'denied';
|
||||
}
|
||||
|
||||
if (this.permission === 'granted') {
|
||||
logger.info('browserNotificationService', 'Permission already granted');
|
||||
return 'granted';
|
||||
}
|
||||
|
||||
try {
|
||||
const permission = await Notification.requestPermission();
|
||||
this.permission = permission;
|
||||
logger.info('browserNotificationService', `Permission ${permission}`);
|
||||
return permission;
|
||||
} catch (error) {
|
||||
logger.error('browserNotificationService', 'requestPermission', error);
|
||||
return 'denied';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送浏览器通知
|
||||
* @param {Object} options 通知选项
|
||||
* @param {string} options.title 标题
|
||||
* @param {string} options.body 内容
|
||||
* @param {string} options.icon 图标路径
|
||||
* @param {string} options.tag 标签(防止重复)
|
||||
* @param {boolean} options.requireInteraction 是否需要用户交互才关闭
|
||||
* @param {Object} options.data 自定义数据(如跳转链接)
|
||||
* @param {number} options.autoClose 自动关闭时间(毫秒)
|
||||
* @returns {Notification|null} 通知对象
|
||||
*/
|
||||
sendNotification({
|
||||
title,
|
||||
body,
|
||||
icon = '/logo192.png',
|
||||
tag,
|
||||
requireInteraction = false,
|
||||
data = {},
|
||||
autoClose = 0,
|
||||
}) {
|
||||
if (!this.isSupported()) {
|
||||
logger.warn('browserNotificationService', 'Notifications not supported');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.permission !== 'granted') {
|
||||
logger.warn('browserNotificationService', 'Permission not granted');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 关闭相同 tag 的旧通知
|
||||
if (tag && this.activeNotifications.has(tag)) {
|
||||
const oldNotification = this.activeNotifications.get(tag);
|
||||
oldNotification.close();
|
||||
}
|
||||
|
||||
// 创建通知
|
||||
const notification = new Notification(title, {
|
||||
body,
|
||||
icon,
|
||||
badge: '/badge.png',
|
||||
tag: tag || `notification_${Date.now()}`,
|
||||
requireInteraction,
|
||||
data,
|
||||
silent: false, // 允许声音
|
||||
});
|
||||
|
||||
// 存储通知引用
|
||||
if (tag) {
|
||||
this.activeNotifications.set(tag, notification);
|
||||
}
|
||||
|
||||
// 自动关闭
|
||||
if (autoClose > 0 && !requireInteraction) {
|
||||
setTimeout(() => {
|
||||
notification.close();
|
||||
}, autoClose);
|
||||
}
|
||||
|
||||
// 通知关闭时清理引用
|
||||
notification.onclose = () => {
|
||||
if (tag) {
|
||||
this.activeNotifications.delete(tag);
|
||||
}
|
||||
};
|
||||
|
||||
logger.info('browserNotificationService', 'Notification sent', { title, tag });
|
||||
return notification;
|
||||
} catch (error) {
|
||||
logger.error('browserNotificationService', 'sendNotification', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置通知点击处理
|
||||
* @param {Notification} notification 通知对象
|
||||
* @param {Function} navigate React Router navigate 函数
|
||||
*/
|
||||
setupClickHandler(notification, navigate) {
|
||||
if (!notification) return;
|
||||
|
||||
notification.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
// 聚焦窗口
|
||||
window.focus();
|
||||
|
||||
// 跳转链接
|
||||
if (notification.data?.link) {
|
||||
navigate(notification.data.link);
|
||||
}
|
||||
|
||||
// 关闭通知
|
||||
notification.close();
|
||||
|
||||
logger.info('browserNotificationService', 'Notification clicked', notification.data);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有活跃通知
|
||||
*/
|
||||
closeAll() {
|
||||
this.activeNotifications.forEach(notification => {
|
||||
notification.close();
|
||||
});
|
||||
this.activeNotifications.clear();
|
||||
logger.info('browserNotificationService', 'All notifications closed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据通知数据发送浏览器通知
|
||||
* @param {Object} notificationData 通知数据
|
||||
* @param {Function} navigate React Router navigate 函数
|
||||
*/
|
||||
sendFromNotificationData(notificationData, navigate) {
|
||||
const { type, priority, title, content, link, extra } = notificationData;
|
||||
|
||||
// 生成唯一 tag
|
||||
const tag = `${type}_${Date.now()}`;
|
||||
|
||||
// 判断是否需要用户交互(紧急通知不自动关闭)
|
||||
const requireInteraction = priority === 'urgent';
|
||||
|
||||
// 发送通知
|
||||
const notification = this.sendNotification({
|
||||
title: title || '新通知',
|
||||
body: content || '',
|
||||
tag,
|
||||
requireInteraction,
|
||||
data: { link, ...extra },
|
||||
autoClose: requireInteraction ? 0 : 8000, // 紧急通知不自动关闭
|
||||
});
|
||||
|
||||
// 设置点击处理
|
||||
if (notification && navigate) {
|
||||
this.setupClickHandler(notification, navigate);
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const browserNotificationService = new BrowserNotificationService();
|
||||
|
||||
export default browserNotificationService;
|
||||
@@ -1,61 +1,303 @@
|
||||
// src/services/mockSocketService.js
|
||||
/**
|
||||
* Mock Socket 服务 - 用于开发环境模拟实时推送
|
||||
* 模拟交易提醒、系统通知等实时消息推送
|
||||
* 模拟金融资讯、事件动向、分析报告等实时消息推送
|
||||
*/
|
||||
|
||||
import { logger } from '../utils/logger';
|
||||
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '../constants/notificationTypes';
|
||||
|
||||
// 模拟交易提醒数据
|
||||
const mockTradeAlerts = [
|
||||
// 模拟金融资讯数据
|
||||
const mockFinancialNews = [
|
||||
// ========== 公告通知 ==========
|
||||
{
|
||||
type: 'trade_alert',
|
||||
severity: 'success',
|
||||
title: '买入成功',
|
||||
message: '您的订单已成功执行:买入 贵州茅台(600519) 100股,成交价 ¥1,850.00',
|
||||
timestamp: Date.now(),
|
||||
autoClose: 8000,
|
||||
},
|
||||
{
|
||||
type: 'trade_alert',
|
||||
severity: 'warning',
|
||||
title: '价格预警',
|
||||
message: '您关注的股票 比亚迪(002594) 当前价格 ¥245.50,已触达预设价格',
|
||||
timestamp: Date.now(),
|
||||
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
|
||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
||||
title: '贵州茅台发布2024年度财报公告',
|
||||
content: '2024年度营收同比增长15.2%,净利润创历史新高,董事会建议每10股派息180元',
|
||||
publishTime: new Date('2024-03-28T15:30:00').getTime(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/event-detail/ann001',
|
||||
extra: {
|
||||
announcementType: '财报',
|
||||
companyCode: '600519',
|
||||
companyName: '贵州茅台',
|
||||
},
|
||||
autoClose: 10000,
|
||||
},
|
||||
{
|
||||
type: 'trade_alert',
|
||||
severity: 'info',
|
||||
title: '持仓提醒',
|
||||
message: '您持有的 宁德时代(300750) 今日涨幅达 5.2%,当前盈利 +¥12,350',
|
||||
timestamp: Date.now(),
|
||||
autoClose: 8000,
|
||||
},
|
||||
{
|
||||
type: 'trade_alert',
|
||||
severity: 'error',
|
||||
title: '委托失败',
|
||||
message: '卖出订单失败:五粮液(000858) 当前处于停牌状态,无法交易',
|
||||
timestamp: Date.now(),
|
||||
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
|
||||
priority: PRIORITY_LEVELS.URGENT,
|
||||
title: '宁德时代发布重大资产重组公告',
|
||||
content: '公司拟收购某新能源材料公司100%股权,交易金额约120亿元,预计增厚业绩20%',
|
||||
publishTime: new Date('2024-03-28T09:00:00').getTime(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/event-detail/ann002',
|
||||
extra: {
|
||||
announcementType: '重组',
|
||||
companyCode: '300750',
|
||||
companyName: '宁德时代',
|
||||
},
|
||||
autoClose: 12000,
|
||||
},
|
||||
{
|
||||
type: 'system_notification',
|
||||
severity: 'info',
|
||||
title: '系统公告',
|
||||
message: '市场将于15:00收盘,请注意及时调整持仓',
|
||||
timestamp: Date.now(),
|
||||
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
|
||||
priority: PRIORITY_LEVELS.NORMAL,
|
||||
title: '中国平安发布分红派息公告',
|
||||
content: '2023年度利润分配方案:每10股派发现金红利23.0元(含税),分红率达30.5%',
|
||||
publishTime: new Date('2024-03-27T16:00:00').getTime(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/event-detail/ann003',
|
||||
extra: {
|
||||
announcementType: '分红',
|
||||
companyCode: '601318',
|
||||
companyName: '中国平安',
|
||||
},
|
||||
autoClose: 10000,
|
||||
},
|
||||
|
||||
// ========== 股票动向 ==========
|
||||
{
|
||||
type: NOTIFICATION_TYPES.STOCK_ALERT,
|
||||
priority: PRIORITY_LEVELS.URGENT,
|
||||
title: '您关注的股票触发预警',
|
||||
content: '宁德时代(300750) 当前价格 ¥245.50,盘中涨幅达 +5.2%,已触达您设置的目标价位',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/stock-overview?code=300750',
|
||||
extra: {
|
||||
stockCode: '300750',
|
||||
stockName: '宁德时代',
|
||||
priceChange: '+5.2%',
|
||||
currentPrice: '245.50',
|
||||
triggerType: '目标价',
|
||||
},
|
||||
autoClose: 10000,
|
||||
},
|
||||
{
|
||||
type: 'trade_alert',
|
||||
severity: 'success',
|
||||
title: '分红到账',
|
||||
message: '您持有的 中国平安(601318) 分红已到账,金额 ¥560.00',
|
||||
timestamp: Date.now(),
|
||||
type: NOTIFICATION_TYPES.STOCK_ALERT,
|
||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
||||
title: '您关注的股票异常波动',
|
||||
content: '比亚迪(002594) 5分钟内跌幅达 -3.8%,当前价格 ¥198.20,建议关注',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/stock-overview?code=002594',
|
||||
extra: {
|
||||
stockCode: '002594',
|
||||
stockName: '比亚迪',
|
||||
priceChange: '-3.8%',
|
||||
currentPrice: '198.20',
|
||||
triggerType: '异常波动',
|
||||
},
|
||||
autoClose: 10000,
|
||||
},
|
||||
{
|
||||
type: NOTIFICATION_TYPES.STOCK_ALERT,
|
||||
priority: PRIORITY_LEVELS.NORMAL,
|
||||
title: '持仓股票表现',
|
||||
content: '隆基绿能(601012) 今日表现优异,涨幅 +4.5%,您当前持仓浮盈 +¥8,200',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/trading-simulation',
|
||||
extra: {
|
||||
stockCode: '601012',
|
||||
stockName: '隆基绿能',
|
||||
priceChange: '+4.5%',
|
||||
profit: '+8200',
|
||||
},
|
||||
autoClose: 8000,
|
||||
},
|
||||
|
||||
// ========== 事件动向 ==========
|
||||
{
|
||||
type: NOTIFICATION_TYPES.EVENT_ALERT,
|
||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
||||
title: '央行宣布降准0.5个百分点',
|
||||
content: '中国人民银行宣布下调金融机构存款准备金率0.5个百分点,释放长期资金约1万亿元,利好股市',
|
||||
publishTime: new Date('2024-03-28T09:00:00').getTime(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/event-detail/evt001',
|
||||
extra: {
|
||||
eventId: 'evt001',
|
||||
relatedStocks: 12,
|
||||
impactLevel: '重大利好',
|
||||
sectors: ['银行', '地产', '基建'],
|
||||
},
|
||||
autoClose: 12000,
|
||||
},
|
||||
{
|
||||
type: NOTIFICATION_TYPES.EVENT_ALERT,
|
||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
||||
title: '新能源汽车补贴政策延期',
|
||||
content: '财政部宣布新能源汽车购置补贴政策延长至2024年底,涉及比亚迪、理想汽车等5家龙头企业',
|
||||
publishTime: new Date('2024-03-28T10:30:00').getTime(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/event-detail/evt002',
|
||||
extra: {
|
||||
eventId: 'evt002',
|
||||
relatedStocks: 5,
|
||||
impactLevel: '重大利好',
|
||||
sectors: ['新能源汽车'],
|
||||
},
|
||||
autoClose: 12000,
|
||||
},
|
||||
{
|
||||
type: NOTIFICATION_TYPES.EVENT_ALERT,
|
||||
priority: PRIORITY_LEVELS.NORMAL,
|
||||
title: '芯片产业扶持政策出台',
|
||||
content: '工信部发布《半导体产业发展指导意见》,未来三年投入500亿专项资金支持芯片研发',
|
||||
publishTime: new Date('2024-03-27T14:00:00').getTime(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/event-detail/evt003',
|
||||
extra: {
|
||||
eventId: 'evt003',
|
||||
relatedStocks: 8,
|
||||
impactLevel: '中长期利好',
|
||||
sectors: ['半导体', '芯片设计'],
|
||||
},
|
||||
autoClose: 10000,
|
||||
},
|
||||
|
||||
// ========== 预测通知 ==========
|
||||
{
|
||||
type: NOTIFICATION_TYPES.EVENT_ALERT,
|
||||
priority: PRIORITY_LEVELS.NORMAL,
|
||||
title: '【预测】央行可能宣布降准政策',
|
||||
content: '基于最新宏观数据分析,预计央行将在本周宣布降准0.5个百分点,释放长期资金',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: true,
|
||||
clickable: false, // ❌ 不可点击
|
||||
link: null,
|
||||
extra: {
|
||||
isPrediction: true,
|
||||
statusHint: '详细报告生成中...',
|
||||
relatedPredictionId: 'pred_001',
|
||||
},
|
||||
autoClose: 15000,
|
||||
},
|
||||
{
|
||||
type: NOTIFICATION_TYPES.EVENT_ALERT,
|
||||
priority: PRIORITY_LEVELS.NORMAL,
|
||||
title: '【预测】新能源补贴政策或将延期',
|
||||
content: '根据政策趋势分析,财政部可能宣布新能源汽车购置补贴政策延长至2025年底',
|
||||
publishTime: Date.now(),
|
||||
pushTime: Date.now(),
|
||||
isAIGenerated: true,
|
||||
clickable: false, // ❌ 不可点击
|
||||
link: null,
|
||||
extra: {
|
||||
isPrediction: true,
|
||||
statusHint: '详细报告生成中...',
|
||||
relatedPredictionId: 'pred_002',
|
||||
},
|
||||
autoClose: 15000,
|
||||
},
|
||||
|
||||
// ========== 分析报告 ==========
|
||||
{
|
||||
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
|
||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
||||
title: '医药行业深度报告:创新药迎来政策拐点',
|
||||
content: 'CXO板块持续受益于全球创新药研发外包需求,建议关注药明康德、凯莱英等龙头企业',
|
||||
publishTime: new Date('2024-03-28T08:00:00').getTime(),
|
||||
pushTime: Date.now(),
|
||||
author: {
|
||||
name: '李明',
|
||||
organization: '中信证券',
|
||||
},
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/forecast-report?id=rpt001',
|
||||
extra: {
|
||||
reportType: '行业研报',
|
||||
industry: '医药',
|
||||
rating: '强烈推荐',
|
||||
},
|
||||
autoClose: 12000,
|
||||
},
|
||||
{
|
||||
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
|
||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
||||
title: 'AI产业链投资机会分析',
|
||||
content: '随着大模型应用加速落地,算力、数据、应用三大方向均存在投资机会,重点关注海光信息、寒武纪',
|
||||
publishTime: new Date('2024-03-28T07:30:00').getTime(),
|
||||
pushTime: Date.now(),
|
||||
author: {
|
||||
name: '王芳',
|
||||
organization: '招商证券',
|
||||
},
|
||||
isAIGenerated: true,
|
||||
clickable: true,
|
||||
link: '/forecast-report?id=rpt002',
|
||||
extra: {
|
||||
reportType: '策略报告',
|
||||
industry: '人工智能',
|
||||
rating: '推荐',
|
||||
},
|
||||
autoClose: 12000,
|
||||
},
|
||||
{
|
||||
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
|
||||
priority: PRIORITY_LEVELS.NORMAL,
|
||||
title: '比亚迪:新能源汽车龙头业绩持续超预期',
|
||||
content: '2024年销量目标400万辆,海外市场拓展顺利,维持"买入"评级,目标价280元',
|
||||
publishTime: new Date('2024-03-27T09:00:00').getTime(),
|
||||
pushTime: Date.now(),
|
||||
author: {
|
||||
name: '张伟',
|
||||
organization: '国泰君安',
|
||||
},
|
||||
isAIGenerated: false,
|
||||
clickable: true,
|
||||
link: '/forecast-report?id=rpt003',
|
||||
extra: {
|
||||
reportType: '公司研报',
|
||||
industry: '新能源汽车',
|
||||
rating: '买入',
|
||||
targetPrice: '280',
|
||||
},
|
||||
autoClose: 10000,
|
||||
},
|
||||
{
|
||||
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
|
||||
priority: PRIORITY_LEVELS.NORMAL,
|
||||
title: '2024年A股市场展望:结构性行情延续',
|
||||
content: 'AI应用、高端制造、自主可控三大主线贯穿全年,建议关注科技成长板块配置机会',
|
||||
publishTime: new Date('2024-03-26T16:00:00').getTime(),
|
||||
pushTime: Date.now(),
|
||||
author: {
|
||||
name: 'AI分析师',
|
||||
organization: '价值前沿',
|
||||
},
|
||||
isAIGenerated: true,
|
||||
clickable: true,
|
||||
link: '/forecast-report?id=rpt004',
|
||||
extra: {
|
||||
reportType: '策略报告',
|
||||
industry: '市场策略',
|
||||
rating: '谨慎乐观',
|
||||
},
|
||||
autoClose: 10000,
|
||||
},
|
||||
];
|
||||
|
||||
class MockSocketService {
|
||||
@@ -194,9 +436,9 @@ class MockSocketService {
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// 从模拟数据中随机选择一条
|
||||
const randomIndex = Math.floor(Math.random() * mockTradeAlerts.length);
|
||||
const randomIndex = Math.floor(Math.random() * mockFinancialNews.length);
|
||||
const alert = {
|
||||
...mockTradeAlerts[randomIndex],
|
||||
...mockFinancialNews[randomIndex],
|
||||
timestamp: Date.now(),
|
||||
id: `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
};
|
||||
|
||||
@@ -61,8 +61,8 @@ import CardHeader from '../../../components/Card/CardHeader';
|
||||
// 导入图表组件
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
|
||||
// 导入导航栏组件
|
||||
import HomeNavbar from '../../../components/Navbars/HomeNavbar';
|
||||
// Navigation bar now provided by MainLayout
|
||||
// import HomeNavbar from '../../../components/Navbars/HomeNavbar';
|
||||
|
||||
import { logger } from '../../../utils/logger';
|
||||
import { getApiBase } from '../../../utils/apiConfig';
|
||||
@@ -473,9 +473,8 @@ const LimitAnalyse = () => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* 添加导航栏 */}
|
||||
<HomeNavbar />
|
||||
|
||||
{/* Navigation bar provided by MainLayout */}
|
||||
|
||||
{/* 添加容器和边距 */}
|
||||
<Container maxW="container.xl" px={6} py={8}>
|
||||
<VStack spacing={6} align="stretch">
|
||||
|
||||
@@ -65,7 +65,8 @@ import EventHeader from './components/EventHeader';
|
||||
import RelatedConcepts from './components/RelatedConcepts';
|
||||
import HistoricalEvents from './components/HistoricalEvents';
|
||||
import RelatedStocks from './components/RelatedStocks';
|
||||
import HomeNavbar from '../../components/Navbars/HomeNavbar';
|
||||
// Navigation bar now provided by MainLayout
|
||||
// import HomeNavbar from '../../components/Navbars/HomeNavbar';
|
||||
import SubscriptionUpgradeModal from '../../components/SubscriptionUpgradeModal';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useSubscription } from '../../hooks/useSubscription';
|
||||
@@ -563,7 +564,7 @@ const EventDetail = () => {
|
||||
// 主要内容
|
||||
return (
|
||||
<Box bg={bgColor} minH="100vh" w="100%">
|
||||
<HomeNavbar />
|
||||
{/* Navigation bar provided by MainLayout */}
|
||||
<Container maxW="7xl" py={8}>
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 事件基本信息 */}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
// src/views/Home/HomePage.js - 专业投资分析平台
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Heading,
|
||||
Text,
|
||||
Card,
|
||||
CardBody,
|
||||
Badge,
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Heading,
|
||||
Text,
|
||||
Card,
|
||||
CardBody,
|
||||
Badge,
|
||||
Button,
|
||||
Flex,
|
||||
VStack,
|
||||
HStack,
|
||||
SimpleGrid,
|
||||
Link
|
||||
Link,
|
||||
useBreakpointValue
|
||||
} from '@chakra-ui/react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -26,6 +27,15 @@ export default function HomePage() {
|
||||
const navigate = useNavigate();
|
||||
const [imageLoaded, setImageLoaded] = React.useState(false);
|
||||
|
||||
// 响应式配置
|
||||
const heroHeight = useBreakpointValue({ base: '60vh', md: '80vh', lg: '100vh' });
|
||||
const headingSize = useBreakpointValue({ base: 'xl', md: '3xl', lg: '4xl' });
|
||||
const headingLetterSpacing = useBreakpointValue({ base: '-1px', md: '-1.5px', lg: '-2px' });
|
||||
const heroTextSize = useBreakpointValue({ base: 'md', md: 'lg', lg: 'xl' });
|
||||
const containerPx = useBreakpointValue({ base: 10, md: 10, lg: 10 });
|
||||
const showDecorations = useBreakpointValue({ base: false, md: true });
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
|
||||
// 保留原有的调试信息
|
||||
useEffect(() => {
|
||||
logger.debug('HomePage', 'AuthContext状态', {
|
||||
@@ -128,7 +138,7 @@ export default function HomePage() {
|
||||
{/* Hero Section - 深色科技风格 */}
|
||||
<Box
|
||||
position="relative"
|
||||
minH="100vh"
|
||||
minH={heroHeight}
|
||||
bg="linear-gradient(135deg, #0E0C15 0%, #15131D 50%, #252134 100%)"
|
||||
overflow="hidden"
|
||||
>
|
||||
@@ -164,59 +174,63 @@ export default function HomePage() {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 装饰性几何图形 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="20%"
|
||||
left="10%"
|
||||
w="200px"
|
||||
h="200px"
|
||||
borderRadius="50%"
|
||||
bg="rgba(255, 215, 0, 0.1)"
|
||||
filter="blur(80px)"
|
||||
className="float-animation"
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="30%"
|
||||
right="20%"
|
||||
w="150px"
|
||||
h="150px"
|
||||
borderRadius="50%"
|
||||
bg="rgba(138, 43, 226, 0.1)"
|
||||
filter="blur(60px)"
|
||||
className="float-animation-reverse"
|
||||
/>
|
||||
{/* 装饰性几何图形 - 移动端隐藏 */}
|
||||
{showDecorations && (
|
||||
<>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="20%"
|
||||
left="10%"
|
||||
w={{ base: '100px', md: '150px', lg: '200px' }}
|
||||
h={{ base: '100px', md: '150px', lg: '200px' }}
|
||||
borderRadius="50%"
|
||||
bg="rgba(255, 215, 0, 0.1)"
|
||||
filter="blur(80px)"
|
||||
className="float-animation"
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="30%"
|
||||
right="20%"
|
||||
w={{ base: '80px', md: '120px', lg: '150px' }}
|
||||
h={{ base: '80px', md: '120px', lg: '150px' }}
|
||||
borderRadius="50%"
|
||||
bg="rgba(138, 43, 226, 0.1)"
|
||||
filter="blur(60px)"
|
||||
className="float-animation-reverse"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Container maxW="7xl" position="relative" zIndex={2}>
|
||||
<VStack spacing={16} align="stretch" minH="100vh" justify="center">
|
||||
<Container maxW="7xl" position="relative" zIndex={2} px={containerPx}>
|
||||
<VStack spacing={{ base: 8, md: 12, lg: 16 }} align="stretch" minH={heroHeight} justify="center">
|
||||
{/* 主标题区域 */}
|
||||
<VStack spacing={6} textAlign="center" pt={8}>
|
||||
<Heading
|
||||
size="4xl"
|
||||
color="white"
|
||||
<VStack spacing={{ base: 4, md: 5, lg: 6 }} textAlign="center" pt={{ base: 4, md: 6, lg: 8 }}>
|
||||
<Heading
|
||||
size={headingSize}
|
||||
color="white"
|
||||
fontWeight="900"
|
||||
letterSpacing="-2px"
|
||||
letterSpacing={headingLetterSpacing}
|
||||
lineHeight="shorter"
|
||||
>
|
||||
智能投资分析平台
|
||||
</Heading>
|
||||
<Text fontSize="xl" color="whiteAlpha.800" maxW="3xl" lineHeight="tall">
|
||||
<Text fontSize={heroTextSize} color="whiteAlpha.800" maxW={{ base: '100%', md: '2xl', lg: '3xl' }} lineHeight="tall" px={{ base: 4, md: 0 }}>
|
||||
专业投资研究工具,助您把握市场机遇
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
|
||||
{/* 核心功能面板 */}
|
||||
<Box>
|
||||
<VStack spacing={8}>
|
||||
<Box pb={{ base: 8, md: 12 }}>
|
||||
<VStack spacing={{ base: 6, md: 8 }}>
|
||||
|
||||
{/* 新闻催化分析 - 突出显示 */}
|
||||
<Card
|
||||
bg="transparent"
|
||||
border="2px solid"
|
||||
borderColor="yellow.400"
|
||||
borderRadius="3xl"
|
||||
borderRadius={{ base: '2xl', md: '3xl' }}
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
shadow="2xl"
|
||||
@@ -232,46 +246,85 @@ export default function HomePage() {
|
||||
zIndex: 0
|
||||
}}
|
||||
>
|
||||
<CardBody p={8} position="relative" zIndex={1}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<HStack spacing={6}>
|
||||
<Box
|
||||
p={4}
|
||||
borderRadius="xl"
|
||||
bg="yellow.400"
|
||||
color="black"
|
||||
>
|
||||
<Text fontSize="3xl">{coreFeatures[0].icon}</Text>
|
||||
</Box>
|
||||
<VStack align="start" spacing={2}>
|
||||
<HStack>
|
||||
<Heading size="xl" color="white">
|
||||
<CardBody p={{ base: 6, md: 8 }} position="relative" zIndex={1}>
|
||||
{isMobile ? (
|
||||
/* 移动端:垂直布局 */
|
||||
<VStack spacing={4} align="stretch">
|
||||
<HStack spacing={4}>
|
||||
<Box
|
||||
p={3}
|
||||
borderRadius="lg"
|
||||
bg="yellow.400"
|
||||
color="black"
|
||||
>
|
||||
<Text fontSize="2xl">{coreFeatures[0].icon}</Text>
|
||||
</Box>
|
||||
<VStack align="start" spacing={1} flex={1}>
|
||||
<Heading size="lg" color="white">
|
||||
{coreFeatures[0].title}
|
||||
</Heading>
|
||||
<Badge colorScheme="yellow" variant="solid" fontSize="sm">
|
||||
<Badge colorScheme="yellow" variant="solid" fontSize="xs">
|
||||
{coreFeatures[0].badge}
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Text color="whiteAlpha.800" fontSize="lg" maxW="md">
|
||||
{coreFeatures[0].description}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<Button
|
||||
colorScheme="yellow"
|
||||
size="lg"
|
||||
borderRadius="full"
|
||||
fontWeight="bold"
|
||||
onClick={() => handleProductClick(coreFeatures[0].url)}
|
||||
>
|
||||
进入功能 →
|
||||
</Button>
|
||||
</Flex>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<Text color="whiteAlpha.800" fontSize="md" lineHeight="tall">
|
||||
{coreFeatures[0].description}
|
||||
</Text>
|
||||
<Button
|
||||
colorScheme="yellow"
|
||||
size="md"
|
||||
borderRadius="full"
|
||||
fontWeight="bold"
|
||||
w="100%"
|
||||
onClick={() => handleProductClick(coreFeatures[0].url)}
|
||||
minH="44px"
|
||||
>
|
||||
进入功能 →
|
||||
</Button>
|
||||
</VStack>
|
||||
) : (
|
||||
/* 桌面端:横向布局 */
|
||||
<Flex align="center" justify="space-between">
|
||||
<HStack spacing={6}>
|
||||
<Box
|
||||
p={4}
|
||||
borderRadius="xl"
|
||||
bg="yellow.400"
|
||||
color="black"
|
||||
>
|
||||
<Text fontSize="3xl">{coreFeatures[0].icon}</Text>
|
||||
</Box>
|
||||
<VStack align="start" spacing={2}>
|
||||
<HStack>
|
||||
<Heading size="xl" color="white">
|
||||
{coreFeatures[0].title}
|
||||
</Heading>
|
||||
<Badge colorScheme="yellow" variant="solid" fontSize="sm">
|
||||
{coreFeatures[0].badge}
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Text color="whiteAlpha.800" fontSize="lg" maxW="md">
|
||||
{coreFeatures[0].description}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<Button
|
||||
colorScheme="yellow"
|
||||
size="lg"
|
||||
borderRadius="full"
|
||||
fontWeight="bold"
|
||||
onClick={() => handleProductClick(coreFeatures[0].url)}
|
||||
>
|
||||
进入功能 →
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 其他5个功能 */}
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6} w="100%">
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={{ base: 4, md: 5, lg: 6 }} w="100%">
|
||||
{coreFeatures.slice(1).map((feature) => (
|
||||
<Card
|
||||
key={feature.id}
|
||||
@@ -279,7 +332,7 @@ export default function HomePage() {
|
||||
backdropFilter="blur(10px)"
|
||||
border="1px solid"
|
||||
borderColor="whiteAlpha.200"
|
||||
borderRadius="2xl"
|
||||
borderRadius={{ base: 'xl', md: '2xl' }}
|
||||
cursor="pointer"
|
||||
transition="all 0.3s ease"
|
||||
_hover={{
|
||||
@@ -288,41 +341,51 @@ export default function HomePage() {
|
||||
transform: 'translateY(-4px)',
|
||||
shadow: '2xl'
|
||||
}}
|
||||
_active={{
|
||||
bg: 'whiteAlpha.200',
|
||||
borderColor: `${feature.color}.400`,
|
||||
transform: 'translateY(-2px)'
|
||||
}}
|
||||
onClick={() => handleProductClick(feature.url)}
|
||||
minH="180px"
|
||||
minH={{ base: 'auto', md: '180px' }}
|
||||
>
|
||||
<CardBody p={6}>
|
||||
<VStack spacing={4} align="start" h="100%">
|
||||
<CardBody p={{ base: 5, md: 6 }}>
|
||||
<VStack spacing={{ base: 3, md: 4 }} align="start" h="100%">
|
||||
<HStack>
|
||||
<Box
|
||||
p={3}
|
||||
p={{ base: 2, md: 3 }}
|
||||
borderRadius="lg"
|
||||
bg={`${feature.color}.50`}
|
||||
border="1px solid"
|
||||
borderColor={`${feature.color}.200`}
|
||||
>
|
||||
<Text fontSize="2xl">{feature.icon}</Text>
|
||||
<Text fontSize={{ base: 'xl', md: '2xl' }}>{feature.icon}</Text>
|
||||
</Box>
|
||||
<Badge colorScheme={feature.color} variant="solid">
|
||||
<Badge colorScheme={feature.color} variant="solid" fontSize={{ base: 'xs', md: 'sm' }}>
|
||||
{feature.badge}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
<VStack align="start" spacing={2} flex={1}>
|
||||
<Heading size="lg" color="white">
|
||||
|
||||
<VStack align="start" spacing={{ base: 1, md: 2 }} flex={1}>
|
||||
<Heading size={{ base: 'md', md: 'lg' }} color="white">
|
||||
{feature.title}
|
||||
</Heading>
|
||||
<Text color="whiteAlpha.800" fontSize="sm" lineHeight="tall">
|
||||
<Text color="whiteAlpha.800" fontSize={{ base: 'xs', md: 'sm' }} lineHeight="tall">
|
||||
{feature.description}
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
|
||||
<Button
|
||||
colorScheme={feature.color}
|
||||
size="sm"
|
||||
size={{ base: 'md', md: 'sm' }}
|
||||
variant="outline"
|
||||
alignSelf="flex-end"
|
||||
onClick={() => handleProductClick(feature.url)}
|
||||
w={{ base: '100%', md: 'auto' }}
|
||||
minH="44px"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleProductClick(feature.url);
|
||||
}}
|
||||
>
|
||||
使用
|
||||
</Button>
|
||||
@@ -338,17 +401,17 @@ export default function HomePage() {
|
||||
</Box>
|
||||
|
||||
{/* 底部区域 */}
|
||||
<Box
|
||||
bg="linear-gradient(135deg, #0E0C15 0%, #15131D 100%)"
|
||||
py={12}
|
||||
<Box
|
||||
bg="linear-gradient(135deg, #0E0C15 0%, #15131D 100%)"
|
||||
py={{ base: 8, md: 12 }}
|
||||
position="relative"
|
||||
>
|
||||
<Container maxW="7xl" position="relative" zIndex={1}>
|
||||
<VStack spacing={6} textAlign="center">
|
||||
<Text color="whiteAlpha.600" fontSize="sm">
|
||||
<Container maxW="7xl" position="relative" zIndex={1} px={containerPx}>
|
||||
<VStack spacing={{ base: 4, md: 6 }} textAlign="center">
|
||||
<Text color="whiteAlpha.600" fontSize={{ base: 'xs', md: 'sm' }}>
|
||||
© 2024 价值前沿. 保留所有权利.
|
||||
</Text>
|
||||
<HStack spacing={4} fontSize="xs" color="whiteAlpha.500">
|
||||
<HStack spacing={{ base: 2, md: 4 }} fontSize="xs" color="whiteAlpha.500" flexWrap="wrap" justify="center">
|
||||
<Link
|
||||
href="https://beian.mps.gov.cn/#/query/webSearch?code=11010802046286"
|
||||
isExternal
|
||||
|
||||
Reference in New Issue
Block a user