概念板块重做

This commit is contained in:
2026-01-05 14:21:51 +08:00
parent 2b40a5a598
commit 4a762b1a22

View File

@@ -1,7 +1,7 @@
// src/components/EventDetailPanel/RelatedConceptsSection/index.js // src/components/EventDetailPanel/RelatedConceptsSection/index.js
// 相关概念区组件 - 概念中心风格卡片 // 相关概念区组件 - 艺术风格卡片
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import {
Box, Box,
@@ -16,28 +16,25 @@ import {
Skeleton, Skeleton,
Card, Card,
CardBody, CardBody,
Image,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { keyframes } from '@emotion/react'; import { keyframes } from '@emotion/react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Layers, ArrowUp, ArrowDown } from 'lucide-react'; import { Layers, ArrowUp, ArrowDown, TrendingUp } from 'lucide-react';
import { logger } from '@utils/logger'; import { logger } from '@utils/logger';
import { getApiBase } from '@utils/apiConfig'; import { getApiBase } from '@utils/apiConfig';
import { selectSubscriptionInfo } from '@store/slices/subscriptionSlice'; import { selectSubscriptionInfo } from '@store/slices/subscriptionSlice';
// 脉冲动画 // 呼吸光效动画
const pulseAnimation = keyframes` const glowAnimation = keyframes`
0% { transform: scale(1); opacity: 1; } 0%, 100% { opacity: 0.4; transform: scale(1); }
50% { transform: scale(1.05); opacity: 0.9; } 50% { opacity: 0.6; transform: scale(1.02); }
100% { transform: scale(1); opacity: 1; }
`; `;
// 毛玻璃效果 // 微光流动动画
const GLASS_BLUR = { const shimmerAnimation = keyframes`
sm: 'blur(4px)', 0% { background-position: -200% center; }
md: 'blur(8px)', 100% { background-position: 200% center; }
lg: 'blur(12px)', `;
};
/** /**
* 格式化涨跌幅 * 格式化涨跌幅
@@ -49,39 +46,51 @@ const formatChangePercent = (value) => {
}; };
/** /**
* 获取涨跌幅颜色 * 获取涨跌幅颜色配置
*/ */
const getChangeColor = (value) => { const getChangeTheme = (value) => {
if (value === null || value === undefined) return 'gray'; if (value === null || value === undefined) {
if (value > 0) return 'red'; return {
if (value < 0) return 'green'; color: 'gray',
return 'gray'; gradient: 'linear(135deg, rgba(71, 85, 105, 0.3) 0%, rgba(51, 65, 85, 0.5) 100%)',
glow: 'rgba(100, 116, 139, 0.3)',
badge: 'rgba(100, 116, 139, 0.9)',
};
}
if (value > 0) {
return {
color: 'red',
gradient: 'linear(135deg, rgba(127, 29, 29, 0.4) 0%, rgba(185, 28, 28, 0.2) 100%)',
glow: 'rgba(239, 68, 68, 0.2)',
badge: 'rgba(220, 38, 38, 0.95)',
};
}
if (value < 0) {
return {
color: 'green',
gradient: 'linear(135deg, rgba(20, 83, 45, 0.4) 0%, rgba(22, 101, 52, 0.2) 100%)',
glow: 'rgba(34, 197, 94, 0.2)',
badge: 'rgba(22, 163, 74, 0.95)',
};
}
return {
color: 'gray',
gradient: 'linear(135deg, rgba(71, 85, 105, 0.3) 0%, rgba(51, 65, 85, 0.5) 100%)',
glow: 'rgba(100, 116, 139, 0.3)',
badge: 'rgba(100, 116, 139, 0.9)',
};
}; };
/** /**
* 概念卡片组件 - 概念中心风格 * 概念卡片组件 - 艺术风格
*/ */
const ConceptCard = ({ concept, onNavigate, isLocked, onLockedClick, index }) => { const ConceptCard = ({ concept, onNavigate, isLocked, onLockedClick, index }) => {
const changePercent = concept.price_info?.avg_change_pct; const changePercent = concept.price_info?.avg_change_pct;
const changeColor = getChangeColor(changePercent); const theme = getChangeTheme(changePercent);
const hasChange = changePercent !== null && changePercent !== undefined; const hasChange = changePercent !== null && changePercent !== undefined;
const stockCount = concept.stock_count || 0; const stockCount = concept.stock_count || 0;
const stocks = concept.stocks || []; const stocks = concept.stocks || [];
// 生成随机涨幅数字背景
const generateNumbersBackground = () => {
const numbers = [];
for (let i = 0; i < 20; i++) {
const isPositive = Math.random() > 0.5;
const value = (Math.random() * 10).toFixed(2);
const sign = isPositive ? '+' : '-';
numbers.push(`${sign}${value}%`);
}
return numbers;
};
const backgroundNumbers = generateNumbersBackground();
const handleClick = () => { const handleClick = () => {
if (isLocked && onLockedClick) { if (isLocked && onLockedClick) {
onLockedClick(); onLockedClick();
@@ -94,173 +103,163 @@ const ConceptCard = ({ concept, onNavigate, isLocked, onLockedClick, index }) =>
<Card <Card
cursor="pointer" cursor="pointer"
onClick={handleClick} onClick={handleClick}
bg="rgba(15, 23, 42, 0.8)" bg="rgba(15, 23, 42, 0.85)"
backdropFilter={GLASS_BLUR.lg} backdropFilter="blur(12px)"
borderWidth="1px" borderWidth="1px"
borderColor="whiteAlpha.100" borderColor="whiteAlpha.100"
overflow="hidden" overflow="hidden"
_hover={{ _hover={{
transform: 'translateY(-4px)', transform: 'translateY(-4px) scale(1.01)',
boxShadow: '0 16px 32px rgba(139, 92, 246, 0.2)', boxShadow: `0 20px 40px ${theme.glow}, 0 0 0 1px rgba(139, 92, 246, 0.3)`,
borderColor: 'purple.500', borderColor: 'purple.400',
'& .card-glow': {
opacity: 0.8,
},
'& .concept-title': {
textShadow: '0 0 30px rgba(255, 255, 255, 0.3)',
},
}} }}
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)" transition="all 0.4s cubic-bezier(0.4, 0, 0.2, 1)"
position="relative" position="relative"
boxShadow="0 4px 16px rgba(0, 0, 0, 0.3)" boxShadow="0 4px 20px rgba(0, 0, 0, 0.4)"
borderRadius="xl" borderRadius="xl"
> >
{/* 排名徽章 */} {/* 排名徽章 */}
{index < 3 && ( {index < 3 && (
<Badge <Badge
position="absolute" position="absolute"
top={2} top={3}
left={2} left={3}
zIndex={10} zIndex={10}
bg={index === 0 ? 'yellow.500' : index === 1 ? 'orange.400' : 'red.400'} bg={index === 0 ? 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%)' : index === 1 ? 'linear-gradient(135deg, #9ca3af 0%, #6b7280 100%)' : 'linear-gradient(135deg, #f97316 0%, #ea580c 100%)'}
color="white" color="white"
borderRadius="full" borderRadius="full"
w="22px" w="24px"
h="22px" h="24px"
display="flex" display="flex"
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
fontSize="xs" fontSize="xs"
fontWeight="bold" fontWeight="bold"
boxShadow="0 2px 8px rgba(0, 0, 0, 0.3)" boxShadow="0 2px 10px rgba(0, 0, 0, 0.3)"
> >
{index + 1} {index + 1}
</Badge> </Badge>
)} )}
{/* 毛玻璃涨幅数字背景 */} {/* 头部区域 - 艺术风格 */}
<Box position="relative" height="100px" overflow="hidden"> <Box position="relative" height="90px" overflow="hidden">
{/* 渐变背景层 - 涨红跌绿 */} {/* 底层渐变 */}
<Box <Box
position="absolute" position="absolute"
top={0} top={0}
left={0} left={0}
right={0} right={0}
bottom={0} bottom={0}
bgGradient={ bgGradient={theme.gradient}
hasChange && changePercent > 0
? "linear(135deg, rgba(153, 27, 27, 0.6) 0%, rgba(239, 68, 68, 0.4) 100%)"
: hasChange && changePercent < 0
? "linear(135deg, rgba(20, 83, 45, 0.6) 0%, rgba(34, 197, 94, 0.4) 100%)"
: "linear(135deg, rgba(71, 85, 105, 0.6) 0%, rgba(100, 116, 139, 0.4) 100%)"
}
/> />
{/* 数字矩阵层 */} {/* 光效层 */}
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
display="grid"
gridTemplateColumns="repeat(4, 1fr)"
gridTemplateRows="repeat(4, 1fr)"
gap={1}
p={2}
opacity={0.08}
>
{backgroundNumbers.map((num, idx) => (
<Flex
key={idx}
align="center"
justify="center"
fontSize="9px"
fontWeight="bold"
color="white"
transform={`rotate(${Math.random() * 16 - 8}deg)`}
>
{num}
</Flex>
))}
</Box>
{/* Logo 水印 */}
<Box <Box
className="card-glow"
position="absolute" position="absolute"
top="50%" top="50%"
left="50%" left="50%"
transform="translate(-50%, -50%)" transform="translate(-50%, -50%)"
width="50px" width="120%"
height="50px" height="120%"
opacity={0.08} bgGradient={`radial(circle at center, ${theme.glow} 0%, transparent 70%)`}
> opacity={0.5}
<Image animation={`${glowAnimation} 3s ease-in-out infinite`}
src={`${process.env.PUBLIC_URL}/LOGO_badge.png`} pointerEvents="none"
alt="Logo" />
width="100%"
height="100%"
objectFit="contain"
/>
</Box>
{/* 高光效果 */} {/* 顶部微光条 */}
<Box <Box
position="absolute" position="absolute"
top={0} top={0}
left={0} left={0}
right={0} right={0}
height="50%" height="2px"
bg="linear-gradient(180deg, rgba(255,255,255,0.06) 0%, transparent 100%)" bg={`linear-gradient(90deg, transparent 0%, ${theme.glow} 50%, transparent 100%)`}
pointerEvents="none" backgroundSize="200% 100%"
animation={`${shimmerAnimation} 3s linear infinite`}
/> />
{/* 涨跌幅 Badge */} {/* 概念标题 - 居中大字 */}
<Flex
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
align="center"
justify="center"
px={4}
>
<Text
className="concept-title"
fontSize="xl"
fontWeight="bold"
color="white"
textAlign="center"
noOfLines={1}
letterSpacing="0.05em"
textShadow="0 2px 10px rgba(0, 0, 0, 0.5)"
transition="text-shadow 0.3s ease"
>
{concept.concept}
</Text>
</Flex>
{/* 涨跌幅徽章 - 右上角 */}
{hasChange && ( {hasChange && (
<Badge <Badge
position="absolute" position="absolute"
top={2} top={3}
right={2} right={3}
bg={changeColor === 'red' ? 'rgba(239, 68, 68, 0.9)' : changeColor === 'green' ? 'rgba(34, 197, 94, 0.9)' : 'rgba(100, 116, 139, 0.9)'} bg={theme.badge}
color="white" color="white"
fontSize="sm" fontSize="xs"
px={2} px={2}
py={0.5} py={0.5}
borderRadius="full" borderRadius="md"
fontWeight="bold" fontWeight="bold"
boxShadow={`0 2px 8px rgba(${changeColor === 'red' ? '239, 68, 68' : changeColor === 'green' ? '34, 197, 94' : '100, 116, 139'}, 0.4)`}
display="flex" display="flex"
alignItems="center" alignItems="center"
gap={0.5} gap={0.5}
animation={Math.abs(changePercent) > 5 ? `${pulseAnimation} 2s infinite` : 'none'} boxShadow="0 2px 8px rgba(0, 0, 0, 0.3)"
> >
<Icon <Icon
as={changePercent > 0 ? ArrowUp : changePercent < 0 ? ArrowDown : null} as={changePercent > 0 ? ArrowUp : changePercent < 0 ? ArrowDown : TrendingUp}
boxSize={3} boxSize={3}
/> />
{formatChangePercent(changePercent)} {formatChangePercent(changePercent)}
</Badge> </Badge>
)} )}
{/* 股票数量徽章 */} {/* 股票数量 - 右下角 */}
<Badge <Badge
position="absolute" position="absolute"
bottom={2} bottom={2}
right={2} right={3}
bg="rgba(0, 0, 0, 0.5)" bg="blackAlpha.600"
backdropFilter={GLASS_BLUR.sm} color="whiteAlpha.900"
color="white"
fontSize="xs" fontSize="xs"
px={2} px={2}
py={0.5} py={0.5}
borderRadius="full" borderRadius="md"
fontWeight="medium" fontWeight="medium"
border="1px solid"
borderColor="whiteAlpha.200"
> >
{stockCount} 只股票 {stockCount}
</Badge> </Badge>
</Box> </Box>
<CardBody p={3} bg="transparent"> <CardBody p={3} bg="transparent">
<VStack align="start" spacing={2}> <VStack align="start" spacing={2}>
{/* 概念名称 */} {/* 描述信息 */}
<Tooltip <Tooltip
label={concept.description || concept.reason || concept.concept} label={concept.description || concept.reason}
placement="top" placement="top"
hasArrow hasArrow
bg="gray.800" bg="gray.800"
@@ -269,86 +268,70 @@ const ConceptCard = ({ concept, onNavigate, isLocked, onLockedClick, index }) =>
borderRadius="md" borderRadius="md"
maxW="300px" maxW="300px"
fontSize="xs" fontSize="xs"
isDisabled={!concept.description && !concept.reason}
> >
<Heading <Text color="whiteAlpha.700" fontSize="xs" noOfLines={2} minH="32px" lineHeight="tall">
size="sm" {concept.description || concept.reason || '暂无描述信息'}
color="white" </Text>
noOfLines={1}
fontWeight="bold"
letterSpacing="0.02em"
>
{concept.concept}
</Heading>
</Tooltip> </Tooltip>
{/* 描述信息 */}
<Text color="whiteAlpha.600" fontSize="xs" noOfLines={2} minH="32px">
{concept.description || concept.reason || '暂无描述信息'}
</Text>
{/* 关联股票预览 */} {/* 关联股票预览 */}
{stocks.length > 0 && ( {stocks.length > 0 && (
<Box <HStack spacing={1} flexWrap="wrap" w="100%">
width="100%" {stocks.slice(0, 3).map((stock, idx) => (
p={2}
bg="whiteAlpha.50"
backdropFilter={GLASS_BLUR.sm}
borderRadius="lg"
>
<HStack spacing={1} flexWrap="wrap">
{stocks.slice(0, 4).map((stock, idx) => (
<Badge
key={idx}
bg="whiteAlpha.100"
color="whiteAlpha.800"
fontSize="10px"
px={1.5}
py={0.5}
borderRadius="sm"
fontWeight="normal"
>
{stock.name}
</Badge>
))}
{stocks.length > 4 && (
<Badge
bg="purple.500"
color="white"
fontSize="10px"
px={1.5}
py={0.5}
borderRadius="sm"
>
+{stocks.length - 4}
</Badge>
)}
</HStack>
</Box>
)}
{/* 层级标签 */}
{concept.hierarchy && (
<HStack spacing={1} flexWrap="wrap">
{concept.hierarchy.lv1 && (
<Badge <Badge
bg="whiteAlpha.200" key={idx}
bg="whiteAlpha.100"
color="whiteAlpha.800" color="whiteAlpha.800"
fontSize="10px" fontSize="10px"
px={1.5} px={1.5}
py={0.5} py={0.5}
borderRadius="sm" borderRadius="sm"
fontWeight="normal"
>
{stock.name}
</Badge>
))}
{stockCount > 3 && (
<Badge
bg="purple.500"
color="white"
fontSize="10px"
px={1.5}
py={0.5}
borderRadius="sm"
>
+{stockCount - 3}
</Badge>
)}
</HStack>
)}
{/* 层级标签 */}
{concept.hierarchy && (concept.hierarchy.lv1 || concept.hierarchy.lv2) && (
<HStack spacing={1} flexWrap="wrap">
{concept.hierarchy.lv1 && (
<Badge
variant="outline"
colorScheme="purple"
fontSize="9px"
px={1.5}
py={0}
borderRadius="sm"
opacity={0.8}
> >
{concept.hierarchy.lv1} {concept.hierarchy.lv1}
</Badge> </Badge>
)} )}
{concept.hierarchy.lv2 && ( {concept.hierarchy.lv2 && (
<Badge <Badge
bg="whiteAlpha.100" variant="outline"
color="whiteAlpha.700" colorScheme="blue"
fontSize="10px" fontSize="9px"
px={1.5} px={1.5}
py={0.5} py={0}
borderRadius="sm" borderRadius="sm"
opacity={0.6}
> >
{concept.hierarchy.lv2} {concept.hierarchy.lv2}
</Badge> </Badge>
@@ -366,25 +349,24 @@ const ConceptCard = ({ concept, onNavigate, isLocked, onLockedClick, index }) =>
*/ */
const SkeletonCard = () => ( const SkeletonCard = () => (
<Card <Card
bg="rgba(15, 23, 42, 0.8)" bg="rgba(15, 23, 42, 0.85)"
borderWidth="1px" borderWidth="1px"
borderColor="whiteAlpha.100" borderColor="whiteAlpha.100"
overflow="hidden" overflow="hidden"
borderRadius="xl" borderRadius="xl"
> >
<Skeleton height="100px" startColor="whiteAlpha.100" endColor="whiteAlpha.200" /> <Skeleton height="90px" startColor="whiteAlpha.100" endColor="whiteAlpha.200" />
<CardBody p={3}> <CardBody p={3}>
<VStack align="stretch" spacing={2}> <VStack align="stretch" spacing={2}>
<Skeleton height="18px" width="70%" startColor="whiteAlpha.100" endColor="whiteAlpha.200" />
<Skeleton height="32px" startColor="whiteAlpha.100" endColor="whiteAlpha.200" /> <Skeleton height="32px" startColor="whiteAlpha.100" endColor="whiteAlpha.200" />
<Skeleton height="28px" startColor="whiteAlpha.100" endColor="whiteAlpha.200" /> <Skeleton height="20px" width="60%" startColor="whiteAlpha.100" endColor="whiteAlpha.200" />
</VStack> </VStack>
</CardBody> </CardBody>
</Card> </Card>
); );
/** /**
* 相关概念区组件 - 概念中心风格 * 相关概念区组件
*/ */
const RelatedConceptsSection = ({ const RelatedConceptsSection = ({
eventId, eventId,
@@ -400,14 +382,11 @@ const RelatedConceptsSection = ({
const [error, setError] = useState(null); const [error, setError] = useState(null);
const navigate = useNavigate(); const navigate = useNavigate();
// 获取订阅信息
const subscriptionInfo = useSelector(selectSubscriptionInfo); const subscriptionInfo = useSelector(selectSubscriptionInfo);
const isSubscriptionExpired = subscriptionInfo.type !== 'free' && !subscriptionInfo.is_active; const isSubscriptionExpired = subscriptionInfo.type !== 'free' && !subscriptionInfo.is_active;
// 颜色配置
const textColor = '#a0aec0'; const textColor = '#a0aec0';
// 获取相关概念
useEffect(() => { useEffect(() => {
const fetchConcepts = async () => { const fetchConcepts = async () => {
if (!eventId) { if (!eventId) {
@@ -463,21 +442,17 @@ const RelatedConceptsSection = ({
fetchConcepts(); fetchConcepts();
}, [eventId, isLocked, isSubscriptionExpired, onConceptsLoaded]); }, [eventId, isLocked, isSubscriptionExpired, onConceptsLoaded]);
// 跳转到概念中心
const handleNavigate = (concept) => { const handleNavigate = (concept) => {
navigate(`/concepts?q=${encodeURIComponent(concept.concept)}`); navigate(`/concepts?q=${encodeURIComponent(concept.concept)}`);
}; };
// 加载中状态
if (loading) { if (loading) {
return ( return (
<Box p={showHeader ? 3 : 0} borderRadius="md"> <Box p={showHeader ? 3 : 0} borderRadius="md">
{showHeader && ( {showHeader && (
<Flex justify="space-between" align="center" mb={3}> <Flex justify="space-between" align="center" mb={3}>
<HStack spacing={2}> <HStack spacing={2}>
<Heading size="sm" color="#e2e8f0"> <Heading size="sm" color="#e2e8f0">相关概念</Heading>
相关概念
</Heading>
{subscriptionBadge} {subscriptionBadge}
</HStack> </HStack>
</Flex> </Flex>
@@ -499,23 +474,13 @@ const RelatedConceptsSection = ({
return ( return (
<Box p={showHeader ? 3 : 0} borderRadius="md"> <Box p={showHeader ? 3 : 0} borderRadius="md">
{/* 标题栏 - 可选 */}
{showHeader && ( {showHeader && (
<Flex justify="space-between" align="center" mb={3}> <Flex justify="space-between" align="center" mb={3}>
<HStack spacing={2}> <HStack spacing={2}>
<Icon as={Layers} color="blue.400" boxSize={4} /> <Icon as={Layers} color="blue.400" boxSize={4} />
<Heading size="sm" color="#e2e8f0"> <Heading size="sm" color="#e2e8f0">相关概念</Heading>
相关概念
</Heading>
{!hasNoConcepts && ( {!hasNoConcepts && (
<Badge <Badge bg="blue.500" color="white" fontSize="xs" px={2} py={0.5} borderRadius="full">
bg="blue.500"
color="white"
fontSize="xs"
px={2}
py={0.5}
borderRadius="full"
>
{concepts.length} {concepts.length}
</Badge> </Badge>
)} )}
@@ -524,7 +489,6 @@ const RelatedConceptsSection = ({
</Flex> </Flex>
)} )}
{/* 概念列表 - 网格布局 */}
{hasNoConcepts ? ( {hasNoConcepts ? (
<Box py={2}> <Box py={2}>
{error ? ( {error ? (