feat: 10.10线上最新代码提交

This commit is contained in:
zdl
2025-10-11 16:16:02 +08:00
parent 4d0dc109bc
commit c1132cd0d6
2750 changed files with 11314 additions and 152745 deletions

View File

@@ -1,7 +1,8 @@
// src/views/EventDetail/components/RelatedConcepts.js - 支持交易日计算
// src/views/EventDetail/components/RelatedConcepts.js - 支持概念API调用
import React, { useState, useEffect } from 'react';
import {
Icon, // 明确导入 Icon 组件
Box,
VStack,
HStack,
@@ -25,37 +26,40 @@ import {
IconButton,
Tooltip,
Button,
Center
Center,
Divider
} from '@chakra-ui/react';
import { FaEye, FaExternalLinkAlt, FaChartLine, FaCalendarAlt } from 'react-icons/fa';
import moment from 'moment';
import tradingDayUtils from '../../../utils/tradingDayUtils'; // 引入交易日工具
// API配置
const API_BASE_URL = process.env.NODE_ENV === 'production' ? '/concept-api' : 'http://111.198.58.126:16801';
const API_BASE_URL = process.env.NODE_ENV === 'production' ? '/concept-api' : 'https://valuefrontier.cn/concept-api';
// ConceptCard 组件 - 修改为使用新的数据结构
// 增强版 ConceptCard 组件 - 展示更多数据细节
const ConceptCard = ({ concept, tradingDate, onViewDetails }) => {
const [imageLoading, setImageLoading] = useState(true);
const [imageError, setImageError] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const cardBg = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const textColor = useColorModeValue('gray.600', 'gray.400');
const highlightBg = useColorModeValue('yellow.50', 'yellow.900');
// 计算涨跌幅颜色
const changeColor = concept.price_info?.avg_change_pct > 0 ? 'red.500' : 'green.500';
// 计算涨跌幅颜色和符号
const changeColor = concept.price_info?.avg_change_pct > 0 ? 'red' : 'green';
const changeSymbol = concept.price_info?.avg_change_pct > 0 ? '+' : '';
const hasValidPriceInfo = concept.price_info && concept.price_info.avg_change_pct !== null;
const handleImageLoad = () => {
setImageLoading(false);
// 获取匹配类型的中文名称
const getMatchTypeName = (type) => {
const typeMap = {
'hybrid_knn': '混合匹配',
'keyword': '关键词匹配',
'semantic': '语义匹配'
};
return typeMap[type] || type;
};
const handleImageError = () => {
setImageLoading(false);
setImageError(true);
};
// 处理概念点击 - 跳转到概念详情页
// 处理概念点击
const handleConceptClick = () => {
window.open(`https://valuefrontier.cn/htmls/${encodeURIComponent(concept.concept)}.html`, '_blank');
};
@@ -64,100 +68,196 @@ const ConceptCard = ({ concept, tradingDate, onViewDetails }) => {
<Card
bg={cardBg}
borderColor={borderColor}
cursor="pointer"
borderWidth={2}
_hover={{
transform: 'translateY(-2px)',
shadow: 'lg',
borderColor: 'blue.300'
shadow: 'xl',
borderColor: 'blue.400'
}}
transition="all 0.2s"
onClick={handleConceptClick}
transition="all 0.3s"
>
<CardBody p={4}>
<VStack spacing={3} align="stretch">
{/* 概念信息 */}
<VStack spacing={2} align="stretch">
<HStack justify="space-between" align="flex-start">
<Text fontSize="md" fontWeight="bold" noOfLines={2}>
{concept.concept}
</Text>
{hasValidPriceInfo ? (
<Tooltip label={`${tradingDate} 平均涨跌幅`}>
<Badge size="sm" colorScheme={concept.price_info.avg_change_pct > 0 ? 'red' : 'green'}>
<CardBody p={5}>
<VStack spacing={4} align="stretch">
{/* 头部信息 */}
<Box>
<HStack justify="space-between" align="flex-start" mb={2}>
<VStack align="start" spacing={1} flex={1}>
<Text fontSize="lg" fontWeight="bold" color="blue.600">
{concept.concept}
</Text>
<HStack spacing={2} flexWrap="wrap">
<Badge colorScheme="purple" fontSize="xs">
相关度: {concept.score.toFixed(2)}
</Badge>
<Badge colorScheme="teal" fontSize="xs">
{getMatchTypeName(concept.match_type)}
</Badge>
<Badge colorScheme="orange" fontSize="xs">
{concept.stock_count} 只股票
</Badge>
</HStack>
</VStack>
{hasValidPriceInfo && (
<Box textAlign="right">
<Text fontSize="xs" color={textColor} mb={1}>
{tradingDate || concept.price_info.trade_date}
</Text>
<Badge
size="lg"
colorScheme={changeColor}
fontSize="md"
px={3}
py={1}
>
{changeSymbol}{concept.price_info.avg_change_pct?.toFixed(2)}%
</Badge>
</Tooltip>
) : (
<Tooltip label={tradingDate ? `${tradingDate} 暂无数据` : '暂无涨跌数据'}>
<Badge size="sm" variant="outline" colorScheme="gray">
--%
</Badge>
</Tooltip>
</Box>
)}
</HStack>
</Box>
{/* 股票数量和相关度 */}
<HStack spacing={2}>
<Badge variant="subtle" colorScheme="blue">
{concept.stock_count} 只股票
</Badge>
<Badge variant="subtle" colorScheme="purple">
相关度: {(concept.score * 10).toFixed(1)}
</Badge>
</HStack>
<Divider />
{/* 概念描述 */}
{concept.description && (
<Text fontSize="sm" color={textColor} noOfLines={3}>
{concept.description}
</Text>
)}
{/* 部分股票展示 */}
{concept.stocks && concept.stocks.length > 0 && (
<Box>
<Text fontSize="xs" color={textColor} mb={1}>
相关股票
</Text>
<HStack spacing={1} flexWrap="wrap">
{concept.stocks.slice(0, 3).map((stock, idx) => (
<Badge key={idx} size="sm" variant="outline">
{stock.stock_name}
</Badge>
))}
{concept.stocks.length > 3 && (
<Text fontSize="xs" color={textColor}>
{concept.stock_count}
</Text>
)}
</HStack>
</Box>
)}
<HStack justify="space-between" align="center" pt={2}>
<Text fontSize="xs" color={textColor}>
点击查看概念详情
</Text>
<IconButton
icon={<FaExternalLinkAlt />}
size="sm"
variant="ghost"
aria-label="查看详情"
{/* 概念描述 */}
<Box>
<Text
fontSize="sm"
color={textColor}
noOfLines={isExpanded ? undefined : 4}
lineHeight="1.6"
>
{concept.description}
</Text>
{concept.description && concept.description.length > 200 && (
<Button
size="xs"
variant="link"
colorScheme="blue"
mt={1}
onClick={(e) => {
e.stopPropagation();
handleConceptClick();
setIsExpanded(!isExpanded);
}}
/>
</HStack>
</VStack>
>
{isExpanded ? '收起' : '展开更多'}
</Button>
)}
</Box>
{/* 历史发生时间 */}
{concept.happened_times && concept.happened_times.length > 0 && (
<Box>
<Text fontSize="xs" fontWeight="semibold" mb={2} color={textColor}>
历史触发时间
</Text>
<HStack spacing={2} flexWrap="wrap">
{concept.happened_times.map((time, idx) => (
<Badge key={idx} variant="subtle" colorScheme="gray" fontSize="xs">
{time}
</Badge>
))}
</HStack>
</Box>
)}
{/* 相关股票展示 - 增强版 */}
{concept.stocks && concept.stocks.length > 0 && (
<Box>
<HStack justify="space-between" mb={2}>
<Text fontSize="sm" fontWeight="semibold" color={textColor}>
核心相关股票
</Text>
<Text fontSize="xs" color="gray.500">
{concept.stock_count}
</Text>
</HStack>
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={2}>
{concept.stocks.slice(0, isExpanded ? 8 : 4).map((stock, idx) => (
<Box
key={idx}
p={2}
borderRadius="md"
bg={useColorModeValue('gray.50', 'gray.700')}
fontSize="xs"
>
<HStack justify="space-between">
<Text fontWeight="semibold">
{stock.stock_name}
</Text>
<Badge size="sm" variant="outline">
{stock.stock_code}
</Badge>
</HStack>
{stock.reason && (
<Text fontSize="xs" color={textColor} mt={1} noOfLines={2}>
{stock.reason}
</Text>
)}
</Box>
))}
</SimpleGrid>
{concept.stocks.length > 4 && !isExpanded && (
<Button
size="xs"
variant="ghost"
colorScheme="blue"
mt={2}
onClick={(e) => {
e.stopPropagation();
setIsExpanded(true);
}}
>
查看更多股票
</Button>
)}
</Box>
)}
{/* 操作按钮 */}
<HStack spacing={2} pt={2}>
<Button
size="sm"
colorScheme="blue"
leftIcon={<FaChartLine />}
flex={1}
onClick={(e) => {
e.stopPropagation();
handleConceptClick();
}}
>
查看概念详情
</Button>
<Button
size="sm"
variant="outline"
colorScheme="blue"
leftIcon={<FaEye />}
flex={1}
onClick={(e) => {
e.stopPropagation();
onViewDetails(concept);
}}
>
快速预览
</Button>
</HStack>
</VStack>
</CardBody>
</Card>
);
};
// 主组件 - 修改为从父组件接收事件标题和时间并搜索概念
// 主组件 - 修改为接收事件信息并调用API
const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoading, error: externalError }) => {
// 调试:检查 Icon 组件是否可用
if (typeof Icon === 'undefined') {
console.error('Icon component is not defined! Make sure @chakra-ui/react is properly imported.');
return <div>组件加载错误Icon 组件未定义</div>;
}
const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedConcept, setSelectedConcept] = useState(null);
const [concepts, setConcepts] = useState([]);
@@ -167,37 +267,63 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
const bgColor = useColorModeValue('blue.50', 'blue.900');
const textColor = useColorModeValue('gray.600', 'gray.400');
// 计算有效交易日
useEffect(() => {
if (eventTime) {
const tradingDate = tradingDayUtils.getEffectiveTradingDay(eventTime);
setEffectiveTradingDate(tradingDate);
console.log('事件时间:', eventTime, '-> 有效交易日:', tradingDate);
// 数据验证函数
const validateConceptData = (data) => {
if (!data || typeof data !== 'object') {
throw new Error('Invalid response data format');
}
}, [eventTime]);
// 搜索概念函数
const searchConcepts = async (title, tradingDate) => {
if (!title) {
setConcepts([]);
return;
// 验证新的API格式
if (data.results && Array.isArray(data.results)) {
return data.results.every(item =>
item &&
typeof item === 'object' &&
(item.concept || item.concept_id) &&
typeof item.score === 'number'
);
}
// 验证旧的API格式
if (data.data && data.data.concepts && Array.isArray(data.data.concepts)) {
return data.data.concepts.every(item =>
item &&
typeof item === 'object' &&
(item.concept || item.concept_id) &&
typeof item.score === 'number'
);
}
return false;
};
setLoading(true);
setError(null);
// 搜索相关概念
const searchConcepts = async (title, tradeDate) => {
try {
setLoading(true);
setError(null);
// 确保tradeDate是字符串格式
let formattedTradeDate;
if (typeof tradeDate === 'string') {
formattedTradeDate = tradeDate;
} else if (tradeDate instanceof Date) {
formattedTradeDate = moment(tradeDate).format('YYYY-MM-DD');
} else if (moment.isMoment(tradeDate)) {
formattedTradeDate = tradeDate.format('YYYY-MM-DD');
} else {
console.warn('Invalid tradeDate format:', tradeDate, typeof tradeDate);
formattedTradeDate = moment().format('YYYY-MM-DD');
}
const requestBody = {
query: title,
size: 4, // 返回前4个最相关的概念
size: 4,
page: 1,
sort_by: "_score" // 按相关度排序
sort_by: "_score",
trade_date: formattedTradeDate
};
// 如果有交易日期,添加到请求中
if (tradingDate) {
requestBody.trade_date = tradingDate;
}
console.log('Searching concepts with:', requestBody);
const response = await fetch(`${API_BASE_URL}/search`, {
method: 'POST',
@@ -208,18 +334,35 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
});
if (!response.ok) {
throw new Error(`搜索失败: ${response.status}`);
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setConcepts(data.results || []);
console.log('Concept search response:', data);
// 如果返回了价格日期,更新显示
if (data.price_date && data.price_date !== tradingDate) {
console.log('API返回的实际价格日期:', data.price_date);
// 数据验证
if (!validateConceptData(data)) {
console.warn('Invalid concept data format:', data);
setConcepts([]);
setError('返回的数据格式无效');
return;
}
// 修复适配实际的API响应格式
if (data.results && Array.isArray(data.results)) {
setConcepts(data.results);
// 使用传入的交易日期作为生效日期
setEffectiveTradingDate(formattedTradeDate);
} else if (data.data && data.data.concepts) {
// 保持向后兼容
setConcepts(data.data.concepts);
setEffectiveTradingDate(data.data.trade_date || formattedTradeDate);
} else {
setConcepts([]);
console.warn('No concepts found in response');
}
} catch (err) {
console.error('概念搜索错误:', err);
console.error('Failed to search concepts:', err);
setError(err.message);
setConcepts([]);
} finally {
@@ -227,23 +370,92 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
}
};
// 当事件标题或交易日变化时搜索概念
// 当事件信息变化时调用API搜索概念
useEffect(() => {
if (eventTitle && effectiveTradingDate) {
searchConcepts(eventTitle, effectiveTradingDate);
} else if (eventTitle) {
// 如果没有交易日期,仍然搜索但不带日期参数
searchConcepts(eventTitle, null);
if (eventTitle && eventTime) {
// 格式化日期为 YYYY-MM-DD
let formattedDate;
try {
// eventTime 可能是Date对象或字符串使用 moment 处理
let eventMoment;
// 检查是否是Date对象
if (eventTime instanceof Date) {
eventMoment = moment(eventTime);
} else if (typeof eventTime === 'string') {
eventMoment = moment(eventTime);
} else if (typeof eventTime === 'number') {
eventMoment = moment(eventTime);
} else {
console.warn('Unknown eventTime format:', eventTime, typeof eventTime);
eventMoment = moment();
}
// 确保moment对象有效
if (!eventMoment.isValid()) {
console.warn('Invalid eventTime:', eventTime);
eventMoment = moment();
}
formattedDate = eventMoment.format('YYYY-MM-DD');
// 如果时间是15:00之后获取下一个交易日
if (eventMoment.hour() >= 15) {
// 使用 tradingDayUtils 获取下一个交易日
if (tradingDayUtils && tradingDayUtils.getNextTradingDay) {
const nextTradingDay = tradingDayUtils.getNextTradingDay(formattedDate);
// 确保返回的是字符串格式
if (typeof nextTradingDay === 'string') {
formattedDate = nextTradingDay;
} else if (nextTradingDay instanceof Date) {
formattedDate = moment(nextTradingDay).format('YYYY-MM-DD');
} else {
console.warn('tradingDayUtils.getNextTradingDay returned invalid format:', nextTradingDay);
formattedDate = eventMoment.add(1, 'day').format('YYYY-MM-DD');
}
} else {
// 降级处理:简单地加一天(不考虑周末和节假日)
console.warn('tradingDayUtils.getNextTradingDay not available, using simple date addition');
formattedDate = eventMoment.add(1, 'day').format('YYYY-MM-DD');
}
}
} catch (e) {
console.error('Failed to format event time:', e);
// 使用当前交易日作为fallback
if (tradingDayUtils && tradingDayUtils.getCurrentTradingDay) {
const currentTradingDay = tradingDayUtils.getCurrentTradingDay();
// 确保返回的是字符串格式
if (typeof currentTradingDay === 'string') {
formattedDate = currentTradingDay;
} else if (currentTradingDay instanceof Date) {
formattedDate = moment(currentTradingDay).format('YYYY-MM-DD');
} else {
console.warn('tradingDayUtils.getCurrentTradingDay returned invalid format:', currentTradingDay);
formattedDate = moment().format('YYYY-MM-DD');
}
} else {
formattedDate = moment().format('YYYY-MM-DD');
}
}
searchConcepts(eventTitle, formattedDate);
} else if (!eventTitle) {
console.warn('No event title provided for concept search');
setConcepts([]);
}
}, [eventTitle, effectiveTradingDate]);
}, [eventTitle, eventTime]);
const handleViewDetails = (concept) => {
setSelectedConcept(concept);
onOpen();
};
// 合并加载状态
const isLoading = externalLoading || loading;
const displayError = externalError || error;
// 加载状态
if (loading || externalLoading) {
if (isLoading) {
return (
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
{[1, 2, 3, 4].map((i) => (
@@ -256,11 +468,11 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
}
// 错误状态
if (error || externalError) {
if (displayError) {
return (
<Alert status="error" borderRadius="lg">
<AlertIcon />
加载相关概念失败: {error || externalError}
加载相关概念失败: {displayError}
</Alert>
);
}
@@ -269,7 +481,9 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
if (!concepts || concepts.length === 0) {
return (
<Box textAlign="center" py={8}>
<Text color="gray.500" mb={4}>暂无相关概念数据</Text>
<Text color="gray.500" mb={4}>
{eventTitle ? '未找到相关概念' : '暂无相关概念数据'}
</Text>
<Button
colorScheme="blue"
size="lg"
@@ -291,9 +505,9 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
<FaCalendarAlt color={textColor} />
<Text fontSize="sm" color={textColor}>
涨跌幅数据日期{effectiveTradingDate}
{eventTime && effectiveTradingDate !== eventTime.split(' ')[0] && (
{eventTime && effectiveTradingDate !== moment(eventTime).format('YYYY-MM-DD') && (
<Text as="span" ml={2} fontSize="xs">
(事件发生于 {eventTime}显示下一交易日数据)
(事件发生于 {typeof eventTime === 'object' ? moment(eventTime).format('YYYY-MM-DD HH:mm') : eventTime}显示下一交易日数据)
</Text>
)}
</Text>
@@ -303,9 +517,9 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
{/* 概念卡片网格 */}
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
{concepts.map((concept) => (
{concepts.map((concept, index) => (
<ConceptCard
key={concept.concept_id}
key={concept.concept_id || index}
concept={concept}
tradingDate={effectiveTradingDate}
onViewDetails={handleViewDetails}
@@ -341,73 +555,183 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
</VStack>
</Center>
{/* 概念详情模态框 */}
<Modal isOpen={isOpen} onClose={onClose} size="lg">
{/* 增强版概念详情模态框 */}
<Modal isOpen={isOpen} onClose={onClose} size="3xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>
<ModalContent maxH="90vh">
<ModalHeader borderBottomWidth={1}>
<HStack justify="space-between">
<Text>{selectedConcept?.concept}</Text>
<VStack align="start" spacing={1}>
<Text fontSize="xl">{selectedConcept?.concept}</Text>
<HStack spacing={2}>
<Badge colorScheme="purple">
相关度: {selectedConcept?.score?.toFixed(2)}
</Badge>
<Badge colorScheme="teal">
{selectedConcept?.stock_count} 只股票
</Badge>
</HStack>
</VStack>
{selectedConcept?.price_info && (
<Badge colorScheme={selectedConcept.price_info.avg_change_pct > 0 ? 'red' : 'green'}>
{selectedConcept.price_info.avg_change_pct > 0 ? '+' : ''}
{selectedConcept.price_info.avg_change_pct?.toFixed(2)}%
</Badge>
<VStack align="end" spacing={1}>
<Text fontSize="xs" color="gray.500">
{selectedConcept.price_info.trade_date || '暂无数据'}
</Text>
<Badge
size="lg"
colorScheme={selectedConcept.price_info.avg_change_pct > 0 ? 'red' : 'green'}
fontSize="lg"
px={4}
py={2}
>
{selectedConcept.price_info.avg_change_pct > 0 ? '+' : ''}
{selectedConcept.price_info.avg_change_pct?.toFixed(2) || '0.00'}%
</Badge>
</VStack>
)}
</HStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<VStack spacing={4} align="stretch">
{/* 显示交易日期 */}
{effectiveTradingDate && (
<HStack>
<FaCalendarAlt />
<Text fontSize="sm">
数据日期{effectiveTradingDate}
</Text>
</HStack>
)}
{/* 概念描述 */}
<ModalBody pb={6} overflowY="auto">
<VStack spacing={6} align="stretch">
{/* 概念描述 - 完整版 */}
{selectedConcept?.description && (
<Box>
<Text fontSize="sm" fontWeight="bold" mb={2}>
概念描述:
</Text>
<Text fontSize="sm" color={useColorModeValue('gray.600', 'gray.400')}>
{selectedConcept.description}
</Text>
<HStack mb={3}>
<Icon as={FaChartLine} color="blue.500" />
<Text fontSize="md" fontWeight="bold">
概念解析
</Text>
</HStack>
<Box
p={4}
bg={useColorModeValue('blue.50', 'blue.900')}
borderRadius="md"
>
<Text
fontSize="sm"
color={useColorModeValue('gray.700', 'gray.300')}
lineHeight="1.8"
>
{selectedConcept.description}
</Text>
</Box>
</Box>
)}
{/* 相关股票 */}
{selectedConcept?.stocks && selectedConcept.stocks.length > 0 && (
{/* 历史触发时间线 */}
{selectedConcept?.happened_times && selectedConcept.happened_times.length > 0 && (
<Box>
<Text fontSize="sm" fontWeight="bold" mb={2}>
相关股票 ({selectedConcept.stock_count}):
</Text>
<SimpleGrid columns={2} spacing={2}>
{selectedConcept.stocks.slice(0, 6).map((stock, index) => (
<Badge key={index} p={2} variant="outline">
{stock.stock_name} ({stock.stock_code})
<HStack mb={3}>
<Icon as={FaCalendarAlt} color="purple.500" />
<Text fontSize="md" fontWeight="bold">
历史触发时间
</Text>
</HStack>
<HStack spacing={3} flexWrap="wrap">
{selectedConcept.happened_times.map((time, idx) => (
<Badge
key={idx}
colorScheme="purple"
variant="subtle"
px={3}
py={1}
>
{time}
</Badge>
))}
</SimpleGrid>
</HStack>
</Box>
)}
{/* 查看详情按钮 */}
<Button
colorScheme="blue"
onClick={() => {
window.open(`https://valuefrontier.cn/htmls/${encodeURIComponent(selectedConcept.concept)}.html`, '_blank');
}}
leftIcon={<FaExternalLinkAlt />}
>
查看概念详情页
</Button>
{/* 相关股票详细列表 */}
{selectedConcept?.stocks && selectedConcept.stocks.length > 0 && (
<Box>
<HStack mb={3}>
<Icon as={FaEye} color="green.500" />
<Text fontSize="md" fontWeight="bold">
核心相关股票 ({selectedConcept.stock_count})
</Text>
</HStack>
<Box
maxH="300px"
overflowY="auto"
borderWidth={1}
borderRadius="md"
p={3}
>
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={3}>
{selectedConcept.stocks.map((stock, idx) => (
<Box
key={idx}
p={3}
borderWidth={1}
borderRadius="md"
bg={useColorModeValue('white', 'gray.700')}
_hover={{
bg: useColorModeValue('gray.50', 'gray.600'),
borderColor: 'blue.300'
}}
transition="all 0.2s"
>
<HStack justify="space-between" mb={2}>
<Text fontWeight="bold" fontSize="sm">
{stock.stock_name}
</Text>
<Badge colorScheme="blue" fontSize="xs">
{stock.stock_code}
</Badge>
</HStack>
{stock.reason && (
<Text fontSize="xs" color="gray.600">
{stock.reason}
</Text>
)}
{(stock.行业 || stock.项目) && (
<HStack spacing={2} mt={2}>
{stock.行业 && (
<Badge size="sm" variant="subtle">
{stock.行业}
</Badge>
)}
{stock.项目 && (
<Badge size="sm" variant="subtle" colorScheme="green">
{stock.项目}
</Badge>
)}
</HStack>
)}
</Box>
))}
</SimpleGrid>
</Box>
</Box>
)}
{/* 操作按钮 */}
<HStack spacing={3} pt={4}>
<Button
colorScheme="blue"
size="lg"
flex={1}
onClick={() => {
window.open(`https://valuefrontier.cn/htmls/${encodeURIComponent(selectedConcept.concept)}.html`, '_blank');
}}
leftIcon={<FaExternalLinkAlt />}
>
查看概念详情页
</Button>
<Button
variant="outline"
colorScheme="blue"
size="lg"
flex={1}
onClick={onClose}
>
关闭
</Button>
</HStack>
</VStack>
</ModalBody>
</ModalContent>