Files
vf_react/src/components/EventDetailPanel/RelatedConceptsSection/index.js

227 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/components/EventDetailPanel/RelatedConceptsSection/index.js
// 相关概念区组件 - 便当盒网格布局
import React, { useState, useEffect } from 'react';
import {
Box,
Flex,
Heading,
Center,
Spinner,
Text,
Badge,
SimpleGrid,
HStack,
Tooltip,
useColorModeValue,
} from '@chakra-ui/react';
import { useNavigate } from 'react-router-dom';
import { logger } from '@utils/logger';
import { getApiBase } from '@utils/apiConfig';
/**
* 单个概念卡片组件(便当盒样式)
*/
const ConceptCard = ({ concept, onNavigate, isLocked, onLockedClick }) => {
// 深色主题固定颜色
const cardBg = 'rgba(252, 129, 129, 0.15)'; // 浅红色背景
const cardHoverBg = 'rgba(252, 129, 129, 0.25)';
const borderColor = 'rgba(252, 129, 129, 0.3)';
const conceptColor = '#fc8181'; // 红色文字(与股票涨色一致)
const handleClick = () => {
if (isLocked && onLockedClick) {
onLockedClick();
return;
}
onNavigate(concept);
};
return (
<Tooltip
label={concept.reason || concept.concept}
placement="top"
hasArrow
bg="gray.800"
color="white"
p={2}
borderRadius="md"
maxW="300px"
fontSize="xs"
>
<Box
bg={cardBg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="lg"
px={3}
py={2}
cursor="pointer"
onClick={handleClick}
_hover={{
bg: cardHoverBg,
transform: 'translateY(-1px)',
boxShadow: 'sm',
}}
transition="all 0.15s ease"
textAlign="center"
>
<Text
fontSize="sm"
fontWeight="semibold"
color={conceptColor}
noOfLines={1}
>
{concept.concept}
</Text>
</Box>
</Tooltip>
);
};
/**
* 相关概念区组件
* @param {Object} props
* @param {number} props.eventId - 事件ID用于获取 related_concepts 表数据)
* @param {string} props.eventTitle - 事件标题(备用)
* @param {React.ReactNode} props.subscriptionBadge - 订阅徽章组件(可选)
* @param {boolean} props.isLocked - 是否锁定(需要付费)
* @param {Function} props.onLockedClick - 锁定时的点击回调
*/
const RelatedConceptsSection = ({
eventId,
eventTitle,
subscriptionBadge = null,
isLocked = false,
onLockedClick = null,
}) => {
const [concepts, setConcepts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const navigate = useNavigate();
// 颜色配置 - 使用深色主题固定颜色
const sectionBg = 'transparent';
const headingColor = '#e2e8f0';
const textColor = '#a0aec0';
const countBadgeBg = '#3182ce';
const countBadgeColor = '#ffffff';
// 获取相关概念
useEffect(() => {
const fetchConcepts = async () => {
if (!eventId) {
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
const apiUrl = `${getApiBase()}/api/events/${eventId}/concepts`;
const response = await fetch(apiUrl, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
if (!response.ok) {
if (response.status === 403) {
setConcepts([]);
setLoading(false);
return;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success && Array.isArray(data.data)) {
setConcepts(data.data);
} else {
setConcepts([]);
}
} catch (err) {
console.error('[RelatedConceptsSection] 获取概念失败', err);
logger.error('RelatedConceptsSection', 'fetchConcepts', err);
setError('加载概念数据失败');
setConcepts([]);
} finally {
setLoading(false);
}
};
fetchConcepts();
}, [eventId]);
// 跳转到概念中心
const handleNavigate = (concept) => {
navigate(`/concepts?q=${encodeURIComponent(concept.concept)}`);
};
// 加载中状态
if (loading) {
return (
<Box bg={sectionBg} p={3} borderRadius="md">
<Center py={4}>
<Spinner size="sm" color="blue.500" mr={2} />
<Text color={textColor} fontSize="sm">加载相关概念中...</Text>
</Center>
</Box>
);
}
const hasNoConcepts = !concepts || concepts.length === 0;
return (
<Box bg={sectionBg} p={3} borderRadius="md">
{/* 标题栏 */}
<Flex justify="space-between" align="center" mb={3}>
<HStack spacing={2}>
<Heading size="sm" color={headingColor}>
相关概念
</Heading>
{!hasNoConcepts && (
<Badge
bg={countBadgeBg}
color={countBadgeColor}
fontSize="xs"
px={2}
py={0.5}
borderRadius="full"
>
{concepts.length}
</Badge>
)}
{subscriptionBadge}
</HStack>
</Flex>
{/* 概念列表 - 便当盒网格布局 */}
{hasNoConcepts ? (
<Box py={2}>
{error ? (
<Text color="red.500" fontSize="sm">{error}</Text>
) : (
<Text color={textColor} fontSize="sm">暂无相关概念数据</Text>
)}
</Box>
) : (
<SimpleGrid columns={{ base: 2, sm: 3, md: 4 }} spacing={2}>
{concepts.map((concept, index) => (
<ConceptCard
key={concept.id || index}
concept={concept}
onNavigate={handleNavigate}
isLocked={isLocked}
onLockedClick={onLockedClick}
/>
))}
</SimpleGrid>
)}
</Box>
);
};
export default RelatedConceptsSection;