community增加事件详情
This commit is contained in:
@@ -107,7 +107,7 @@ const CompactMetaBar = ({ event, importance, isFollowing, followerCount, onToggl
|
||||
showCount={true}
|
||||
/>
|
||||
|
||||
{/* 分享按钮 */}
|
||||
{/* 分享按钮 - 复制链接 */}
|
||||
<ShareButton
|
||||
title={event.title}
|
||||
desc={event.description?.slice(0, 100) || ''}
|
||||
@@ -115,16 +115,17 @@ const CompactMetaBar = ({ event, importance, isFollowing, followerCount, onToggl
|
||||
imgUrl={`${window.location.origin}/logo192.png`}
|
||||
variant="icon"
|
||||
size="sm"
|
||||
colorScheme="teal"
|
||||
/>
|
||||
|
||||
{/* 查看详情按钮 */}
|
||||
<Tooltip label="查看事件详情页" hasArrow>
|
||||
<Tooltip label="在新页面查看完整详情" hasArrow>
|
||||
<IconButton
|
||||
icon={<Icon as={ExternalLink} boxSize={4} />}
|
||||
aria-label="查看事件详情"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="blue"
|
||||
color="blue.300"
|
||||
onClick={handleViewDetail}
|
||||
_hover={{
|
||||
bg: 'blue.500',
|
||||
|
||||
@@ -10,19 +10,26 @@ import BasicInfoTab from '../CompanyOverview/BasicInfoTab';
|
||||
import DeepAnalysis from '../DeepAnalysis';
|
||||
import MarketDataView from '../MarketDataView';
|
||||
import FinancialPanorama from '../FinancialPanorama';
|
||||
import ForecastReport from '../ForecastReport';
|
||||
import DynamicTracking from '../DynamicTracking';
|
||||
import ConceptSector from '../ConceptSector';
|
||||
|
||||
/**
|
||||
* Tab 组件映射
|
||||
* 调整后的标签页:
|
||||
* - 深度分析(首位)
|
||||
* - 股票行情
|
||||
* - 概念板块(新增)
|
||||
* - 动态跟踪
|
||||
* - 财务全景
|
||||
* - 公司档案(移到最后)
|
||||
*/
|
||||
const TAB_COMPONENTS = {
|
||||
overview: BasicInfoTab,
|
||||
analysis: DeepAnalysis,
|
||||
market: MarketDataView,
|
||||
financial: FinancialPanorama,
|
||||
forecast: ForecastReport,
|
||||
concept: ConceptSector,
|
||||
tracking: DynamicTracking,
|
||||
financial: FinancialPanorama,
|
||||
overview: BasicInfoTab,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
380
src/views/Company/components/ConceptSector/index.js
Normal file
380
src/views/Company/components/ConceptSector/index.js
Normal file
@@ -0,0 +1,380 @@
|
||||
// src/views/Company/components/ConceptSector/index.js
|
||||
// 个股详情页 - 概念板块 Tab 组件
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Badge,
|
||||
SimpleGrid,
|
||||
Card,
|
||||
CardBody,
|
||||
Skeleton,
|
||||
SkeletonText,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Divider,
|
||||
Button,
|
||||
Tooltip,
|
||||
Tag,
|
||||
TagLabel,
|
||||
Wrap,
|
||||
WrapItem,
|
||||
useColorModeValue,
|
||||
Icon,
|
||||
Flex,
|
||||
Spacer,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
Layers,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
ChevronRight,
|
||||
Calendar,
|
||||
Hash,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { logger } from '@utils/logger';
|
||||
import { getApiBase } from '@utils/apiConfig';
|
||||
import { getConceptHtmlUrl } from '@utils/textUtils';
|
||||
|
||||
// API 配置
|
||||
const API_BASE_URL =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? `${getApiBase()}/concept-api`
|
||||
: 'http://111.198.58.126:16801';
|
||||
|
||||
/**
|
||||
* 单个概念卡片组件
|
||||
*/
|
||||
const ConceptCard = ({ concept, onClick }) => {
|
||||
const cardBg = useColorModeValue('white', 'rgba(26, 32, 44, 0.8)');
|
||||
const borderColor = useColorModeValue('gray.200', 'rgba(255, 195, 0, 0.2)');
|
||||
const textSecondary = useColorModeValue('gray.600', 'gray.400');
|
||||
|
||||
// 涨跌幅颜色
|
||||
const changePct = concept.price_info?.avg_change_pct;
|
||||
const hasChange = changePct !== null && changePct !== undefined;
|
||||
const isPositive = changePct > 0;
|
||||
const changeColor = isPositive ? 'red.500' : changePct < 0 ? 'green.500' : 'gray.500';
|
||||
const changeBgColor = isPositive
|
||||
? 'rgba(239, 68, 68, 0.1)'
|
||||
: changePct < 0
|
||||
? 'rgba(34, 197, 94, 0.1)'
|
||||
: 'rgba(128, 128, 128, 0.1)';
|
||||
|
||||
// 层级信息
|
||||
const hierarchy = concept.hierarchy;
|
||||
const hierarchyPath = hierarchy
|
||||
? [hierarchy.lv1, hierarchy.lv2, hierarchy.lv3].filter(Boolean).join(' > ')
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Card
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="xl"
|
||||
overflow="hidden"
|
||||
cursor="pointer"
|
||||
transition="all 0.3s"
|
||||
_hover={{
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: '0 12px 24px rgba(0, 0, 0, 0.15)',
|
||||
borderColor: 'blue.400',
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<CardBody p={4}>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
{/* 头部:概念名称 + 涨跌幅 */}
|
||||
<HStack justify="space-between" align="flex-start">
|
||||
<VStack align="start" spacing={1} flex={1}>
|
||||
<HStack spacing={2}>
|
||||
<Icon as={Layers} boxSize={4} color="blue.400" />
|
||||
<Text fontSize="md" fontWeight="bold" color="blue.400" noOfLines={1}>
|
||||
{concept.concept}
|
||||
</Text>
|
||||
</HStack>
|
||||
{hierarchyPath && (
|
||||
<Text fontSize="xs" color={textSecondary} noOfLines={1}>
|
||||
{hierarchyPath}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
|
||||
{hasChange && (
|
||||
<Box
|
||||
bg={changeBgColor}
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="md"
|
||||
minW="70px"
|
||||
textAlign="center"
|
||||
>
|
||||
<HStack spacing={1} justify="center">
|
||||
<Icon
|
||||
as={isPositive ? TrendingUp : TrendingDown}
|
||||
boxSize={3}
|
||||
color={changeColor}
|
||||
/>
|
||||
<Text fontSize="sm" fontWeight="bold" color={changeColor}>
|
||||
{isPositive ? '+' : ''}
|
||||
{changePct.toFixed(2)}%
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{/* 描述 */}
|
||||
{concept.description && (
|
||||
<Text fontSize="sm" color={textSecondary} noOfLines={2} lineHeight="1.6">
|
||||
{concept.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* 标签 */}
|
||||
{concept.tags && concept.tags.length > 0 && (
|
||||
<Wrap spacing={1}>
|
||||
{concept.tags.slice(0, 4).map((tag, idx) => (
|
||||
<WrapItem key={idx}>
|
||||
<Tag size="sm" variant="subtle" colorScheme="purple" borderRadius="full">
|
||||
<TagLabel fontSize="xs">{tag}</TagLabel>
|
||||
</Tag>
|
||||
</WrapItem>
|
||||
))}
|
||||
{concept.tags.length > 4 && (
|
||||
<WrapItem>
|
||||
<Tag size="sm" variant="outline" colorScheme="gray" borderRadius="full">
|
||||
<TagLabel fontSize="xs">+{concept.tags.length - 4}</TagLabel>
|
||||
</Tag>
|
||||
</WrapItem>
|
||||
)}
|
||||
</Wrap>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 底部信息 */}
|
||||
<HStack justify="space-between" fontSize="xs" color={textSecondary}>
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Hash} boxSize={3} />
|
||||
<Text>{concept.stock_count || 0} 只成分股</Text>
|
||||
</HStack>
|
||||
{concept.price_info?.trade_date && (
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Calendar} boxSize={3} />
|
||||
<Text>{concept.price_info.trade_date}</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 骨架屏加载组件
|
||||
*/
|
||||
const ConceptSkeleton = () => (
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<Card key={i} borderRadius="xl" overflow="hidden">
|
||||
<CardBody p={4}>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
<HStack justify="space-between">
|
||||
<Skeleton height="24px" width="120px" />
|
||||
<Skeleton height="28px" width="70px" borderRadius="md" />
|
||||
</HStack>
|
||||
<SkeletonText noOfLines={2} spacing={2} />
|
||||
<HStack spacing={2}>
|
||||
<Skeleton height="20px" width="50px" borderRadius="full" />
|
||||
<Skeleton height="20px" width="50px" borderRadius="full" />
|
||||
</HStack>
|
||||
<Divider />
|
||||
<HStack justify="space-between">
|
||||
<Skeleton height="16px" width="80px" />
|
||||
<Skeleton height="16px" width="80px" />
|
||||
</HStack>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
);
|
||||
|
||||
/**
|
||||
* 概念板块组件
|
||||
* @param {Object} props
|
||||
* @param {string} props.stockCode - 股票代码
|
||||
*/
|
||||
const ConceptSector = ({ stockCode }) => {
|
||||
const navigate = useNavigate();
|
||||
const [concepts, setConcepts] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const bgColor = useColorModeValue('gray.50', 'transparent');
|
||||
const headerBg = useColorModeValue('white', 'rgba(26, 32, 44, 0.6)');
|
||||
const textColor = useColorModeValue('gray.800', 'white');
|
||||
const textSecondary = useColorModeValue('gray.600', 'gray.400');
|
||||
|
||||
/**
|
||||
* 获取股票关联的概念
|
||||
*/
|
||||
const fetchStockConcepts = useCallback(async () => {
|
||||
if (!stockCode) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/stock/${stockCode}/concepts?size=50`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setConcepts(data.concepts || []);
|
||||
} catch (err) {
|
||||
logger.error('ConceptSector', 'fetchStockConcepts', err, { stockCode });
|
||||
setError('获取概念数据失败,请稍后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStockConcepts();
|
||||
}, [fetchStockConcepts]);
|
||||
|
||||
/**
|
||||
* 点击概念跳转
|
||||
*/
|
||||
const handleConceptClick = useCallback(
|
||||
(concept) => {
|
||||
// 跳转到概念详情页(外部链接)
|
||||
const url = getConceptHtmlUrl(concept.concept);
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* 跳转到概念中心
|
||||
*/
|
||||
const handleGoToConceptCenter = useCallback(() => {
|
||||
navigate('/concept');
|
||||
}, [navigate]);
|
||||
|
||||
// 按涨跌幅排序概念
|
||||
const sortedConcepts = [...concepts].sort((a, b) => {
|
||||
const aChange = a.price_info?.avg_change_pct ?? -Infinity;
|
||||
const bChange = b.price_info?.avg_change_pct ?? -Infinity;
|
||||
return bChange - aChange;
|
||||
});
|
||||
|
||||
// 统计信息
|
||||
const totalConcepts = concepts.length;
|
||||
const positiveConcepts = concepts.filter(
|
||||
(c) => c.price_info?.avg_change_pct > 0
|
||||
).length;
|
||||
const negativeConcepts = concepts.filter(
|
||||
(c) => c.price_info?.avg_change_pct < 0
|
||||
).length;
|
||||
|
||||
return (
|
||||
<Box bg={bgColor} minH="400px" p={{ base: 3, md: 4 }}>
|
||||
{/* 头部统计信息 */}
|
||||
<Card bg={headerBg} borderRadius="xl" mb={4} boxShadow="sm">
|
||||
<CardBody py={3} px={4}>
|
||||
<Flex align="center" wrap="wrap" gap={4}>
|
||||
<HStack spacing={3}>
|
||||
<Icon as={Sparkles} boxSize={5} color="yellow.400" />
|
||||
<Text fontSize="lg" fontWeight="bold" color={textColor}>
|
||||
概念板块
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack spacing={4} flex={1} justify="center">
|
||||
<HStack spacing={2}>
|
||||
<Text fontSize="sm" color={textSecondary}>
|
||||
共
|
||||
</Text>
|
||||
<Badge colorScheme="blue" fontSize="sm" px={2}>
|
||||
{totalConcepts}
|
||||
</Badge>
|
||||
<Text fontSize="sm" color={textSecondary}>
|
||||
个概念
|
||||
</Text>
|
||||
</HStack>
|
||||
<Divider orientation="vertical" h="20px" />
|
||||
<HStack spacing={1}>
|
||||
<Icon as={TrendingUp} boxSize={4} color="red.400" />
|
||||
<Text fontSize="sm" color="red.400" fontWeight="medium">
|
||||
{positiveConcepts}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing={1}>
|
||||
<Icon as={TrendingDown} boxSize={4} color="green.400" />
|
||||
<Text fontSize="sm" color="green.400" fontWeight="medium">
|
||||
{negativeConcepts}
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
<Spacer display={{ base: 'none', md: 'block' }} />
|
||||
|
||||
<Tooltip label="前往概念中心查看更多">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="blue"
|
||||
rightIcon={<ChevronRight size={16} />}
|
||||
onClick={handleGoToConceptCenter}
|
||||
>
|
||||
概念中心
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 内容区域 */}
|
||||
{loading ? (
|
||||
<ConceptSkeleton />
|
||||
) : error ? (
|
||||
<Alert status="error" borderRadius="xl">
|
||||
<AlertIcon />
|
||||
{error}
|
||||
<Button size="sm" ml={4} onClick={fetchStockConcepts}>
|
||||
重试
|
||||
</Button>
|
||||
</Alert>
|
||||
) : concepts.length === 0 ? (
|
||||
<Alert status="info" borderRadius="xl">
|
||||
<AlertIcon />
|
||||
暂无关联概念数据
|
||||
</Alert>
|
||||
) : (
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
||||
{sortedConcepts.map((concept, index) => (
|
||||
<ConceptCard
|
||||
key={concept.concept_id || index}
|
||||
concept={concept}
|
||||
onClick={() => handleConceptClick(concept)}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConceptSector;
|
||||
@@ -1,19 +1,25 @@
|
||||
// src/views/Company/constants/index.js
|
||||
// 公司详情页面常量配置
|
||||
|
||||
import { LineChart, Banknote, BarChart2, Info, Brain, Newspaper } from 'lucide-react';
|
||||
import { LineChart, Banknote, Info, Brain, Newspaper, Layers } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Tab 配置
|
||||
* 调整:
|
||||
* - 去掉"公司概览"(原 overview)
|
||||
* - 去掉"盈利预测"(原 forecast)
|
||||
* - 深度分析作为首位
|
||||
* - 动态跟踪移到财务全景前面
|
||||
* - 新增"概念板块"
|
||||
* @type {Array<{key: string, name: string, icon: React.ComponentType}>}
|
||||
*/
|
||||
export const COMPANY_TABS = [
|
||||
{ key: 'overview', name: '公司档案', icon: Info },
|
||||
{ key: 'analysis', name: '深度分析', icon: Brain },
|
||||
{ key: 'market', name: '股票行情', icon: LineChart },
|
||||
{ key: 'financial', name: '财务全景', icon: Banknote },
|
||||
{ key: 'forecast', name: '盈利预测', icon: BarChart2 },
|
||||
{ key: 'concept', name: '概念板块', icon: Layers },
|
||||
{ key: 'tracking', name: '动态跟踪', icon: Newspaper },
|
||||
{ key: 'financial', name: '财务全景', icon: Banknote },
|
||||
{ key: 'overview', name: '公司档案', icon: Info },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user