概念板块重做

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