227 lines
5.8 KiB
JavaScript
227 lines
5.8 KiB
JavaScript
// 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;
|