feat: 个股添加个股列表弹窗
This commit is contained in:
13
app.py
13
app.py
@@ -12284,13 +12284,20 @@ def get_daily_top_concepts():
|
|||||||
top_concepts = []
|
top_concepts = []
|
||||||
|
|
||||||
for concept in data.get('results', []):
|
for concept in data.get('results', []):
|
||||||
|
# 保持与 /concept-api/search 相同的字段结构
|
||||||
top_concepts.append({
|
top_concepts.append({
|
||||||
'concept_id': concept.get('concept_id'),
|
'concept_id': concept.get('concept_id'),
|
||||||
'concept_name': concept.get('concept'),
|
'concept': concept.get('concept'), # 原始字段名
|
||||||
|
'concept_name': concept.get('concept'), # 兼容旧字段名
|
||||||
'description': concept.get('description'),
|
'description': concept.get('description'),
|
||||||
'change_percent': concept.get('price_info', {}).get('avg_change_pct', 0),
|
|
||||||
'stock_count': concept.get('stock_count', 0),
|
'stock_count': concept.get('stock_count', 0),
|
||||||
'stocks': concept.get('stocks', [])[:5] # 只返回前5只股票
|
'score': concept.get('score'),
|
||||||
|
'match_type': concept.get('match_type'),
|
||||||
|
'price_info': concept.get('price_info', {}), # 完整的价格信息
|
||||||
|
'change_percent': concept.get('price_info', {}).get('avg_change_pct', 0), # 兼容旧字段
|
||||||
|
'happened_times': concept.get('happened_times', []), # 历史触发时间
|
||||||
|
'stocks': concept.get('stocks', []), # 返回完整股票列表
|
||||||
|
'hot_score': concept.get('hot_score')
|
||||||
})
|
})
|
||||||
|
|
||||||
# 格式化日期为 YYYY-MM-DD
|
# 格式化日期为 YYYY-MM-DD
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const marketHandlers = [
|
|||||||
{ name: '机器人', desc: '人形机器人产业化加速,特斯拉、小米等巨头入局,产业链迎来发展机遇。' },
|
{ name: '机器人', desc: '人形机器人产业化加速,特斯拉、小米等巨头入局,产业链迎来发展机遇。' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 股票池
|
// 股票池(扩展到足够多的股票)
|
||||||
const stockPool = [
|
const stockPool = [
|
||||||
{ stock_code: '600519', stock_name: '贵州茅台' },
|
{ stock_code: '600519', stock_name: '贵州茅台' },
|
||||||
{ stock_code: '300750', stock_name: '宁德时代' },
|
{ stock_code: '300750', stock_name: '宁德时代' },
|
||||||
@@ -104,30 +104,102 @@ export const marketHandlers = [
|
|||||||
{ stock_code: '300274', stock_name: '阳光电源' },
|
{ stock_code: '300274', stock_name: '阳光电源' },
|
||||||
{ stock_code: '688981', stock_name: '中芯国际' },
|
{ stock_code: '688981', stock_name: '中芯国际' },
|
||||||
{ stock_code: '000725', stock_name: '京东方A' },
|
{ stock_code: '000725', stock_name: '京东方A' },
|
||||||
|
{ stock_code: '600036', stock_name: '招商银行' },
|
||||||
|
{ stock_code: '000858', stock_name: '五粮液' },
|
||||||
|
{ stock_code: '601166', stock_name: '兴业银行' },
|
||||||
|
{ stock_code: '600276', stock_name: '恒瑞医药' },
|
||||||
|
{ stock_code: '000333', stock_name: '美的集团' },
|
||||||
|
{ stock_code: '600887', stock_name: '伊利股份' },
|
||||||
|
{ stock_code: '002415', stock_name: '海康威视' },
|
||||||
|
{ stock_code: '601888', stock_name: '中国中免' },
|
||||||
|
{ stock_code: '300059', stock_name: '东方财富' },
|
||||||
|
{ stock_code: '002475', stock_name: '立讯精密' },
|
||||||
|
{ stock_code: '600900', stock_name: '长江电力' },
|
||||||
|
{ stock_code: '601398', stock_name: '工商银行' },
|
||||||
|
{ stock_code: '600030', stock_name: '中信证券' },
|
||||||
|
{ stock_code: '000568', stock_name: '泸州老窖' },
|
||||||
|
{ stock_code: '002352', stock_name: '顺丰控股' },
|
||||||
|
{ stock_code: '600809', stock_name: '山西汾酒' },
|
||||||
|
{ stock_code: '300015', stock_name: '爱尔眼科' },
|
||||||
|
{ stock_code: '002142', stock_name: '宁波银行' },
|
||||||
|
{ stock_code: '601899', stock_name: '紫金矿业' },
|
||||||
|
{ stock_code: '600309', stock_name: '万华化学' },
|
||||||
|
{ stock_code: '002304', stock_name: '洋河股份' },
|
||||||
|
{ stock_code: '600585', stock_name: '海螺水泥' },
|
||||||
|
{ stock_code: '601288', stock_name: '农业银行' },
|
||||||
|
{ stock_code: '600050', stock_name: '中国联通' },
|
||||||
|
{ stock_code: '000001', stock_name: '平安银行' },
|
||||||
|
{ stock_code: '601668', stock_name: '中国建筑' },
|
||||||
|
{ stock_code: '600028', stock_name: '中国石化' },
|
||||||
|
{ stock_code: '601857', stock_name: '中国石油' },
|
||||||
|
{ stock_code: '600000', stock_name: '浦发银行' },
|
||||||
|
{ stock_code: '601328', stock_name: '交通银行' },
|
||||||
|
{ stock_code: '000002', stock_name: '万科A' },
|
||||||
|
{ stock_code: '600104', stock_name: '上汽集团' },
|
||||||
|
{ stock_code: '601601', stock_name: '中国太保' },
|
||||||
|
{ stock_code: '600016', stock_name: '民生银行' },
|
||||||
|
{ stock_code: '601628', stock_name: '中国人寿' },
|
||||||
|
{ stock_code: '600031', stock_name: '三一重工' },
|
||||||
|
{ stock_code: '002230', stock_name: '科大讯飞' },
|
||||||
|
{ stock_code: '300124', stock_name: '汇川技术' },
|
||||||
|
{ stock_code: '002049', stock_name: '紫光国微' },
|
||||||
|
{ stock_code: '688012', stock_name: '中微公司' },
|
||||||
|
{ stock_code: '688008', stock_name: '澜起科技' },
|
||||||
|
{ stock_code: '603501', stock_name: '韦尔股份' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 生成历史触发时间
|
||||||
|
const generateHappenedTimes = (seed) => {
|
||||||
|
const times = [];
|
||||||
|
const count = 3 + (seed % 3); // 3-5个时间点
|
||||||
|
for (let k = 0; k < count; k++) {
|
||||||
|
const daysAgo = 30 + (seed * 7 + k * 11) % 330;
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() - daysAgo);
|
||||||
|
times.push(d.toISOString().split('T')[0]);
|
||||||
|
}
|
||||||
|
return times.sort().reverse();
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchTypes = ['hybrid_knn', 'keyword', 'semantic'];
|
||||||
|
|
||||||
// 生成概念数据
|
// 生成概念数据
|
||||||
const concepts = [];
|
const concepts = [];
|
||||||
for (let i = 0; i < Math.min(limit, conceptPool.length); i++) {
|
for (let i = 0; i < Math.min(limit, conceptPool.length); i++) {
|
||||||
const concept = conceptPool[i];
|
const concept = conceptPool[i];
|
||||||
const changePercent = parseFloat((Math.random() * 8 - 1).toFixed(2)); // -1% ~ 7%
|
const changePercent = parseFloat((Math.random() * 8 - 1).toFixed(2)); // -1% ~ 7%
|
||||||
const stockCount = Math.floor(Math.random() * 40) + 20; // 20-60只股票
|
const stockCount = Math.floor(Math.random() * 20) + 15; // 15-35只股票
|
||||||
|
|
||||||
// 随机选取3-4只相关股票
|
// 生成与 stockCount 一致的股票列表(包含完整字段)
|
||||||
const relatedStocks = [];
|
const relatedStocks = [];
|
||||||
const stockIndices = new Set();
|
for (let j = 0; j < stockCount; j++) {
|
||||||
while (stockIndices.size < Math.min(4, stockPool.length)) {
|
const idx = (i * 7 + j) % stockPool.length;
|
||||||
stockIndices.add(Math.floor(Math.random() * stockPool.length));
|
const stock = stockPool[idx];
|
||||||
|
relatedStocks.push({
|
||||||
|
stock_code: stock.stock_code,
|
||||||
|
stock_name: stock.stock_name,
|
||||||
|
reason: `作为行业龙头企业,${stock.stock_name}在该领域具有核心竞争优势,市场份额领先。`,
|
||||||
|
change_pct: parseFloat((Math.random() * 15 - 5).toFixed(2)) // -5% ~ +10%
|
||||||
|
});
|
||||||
}
|
}
|
||||||
stockIndices.forEach(idx => relatedStocks.push(stockPool[idx]));
|
|
||||||
|
|
||||||
concepts.push({
|
concepts.push({
|
||||||
concept_id: `CONCEPT_${1001 + i}`,
|
concept_id: `CONCEPT_${1001 + i}`,
|
||||||
concept_name: concept.name,
|
concept: concept.name, // 原始字段名
|
||||||
change_percent: changePercent,
|
concept_name: concept.name, // 兼容字段名
|
||||||
stock_count: stockCount,
|
|
||||||
description: concept.desc,
|
description: concept.desc,
|
||||||
stocks: relatedStocks
|
stock_count: stockCount,
|
||||||
|
score: parseFloat((Math.random() * 5 + 3).toFixed(2)), // 3-8 分数
|
||||||
|
match_type: matchTypes[i % 3],
|
||||||
|
price_info: {
|
||||||
|
avg_change_pct: changePercent,
|
||||||
|
avg_price: parseFloat((Math.random() * 100 + 10).toFixed(2)),
|
||||||
|
total_market_cap: parseFloat((Math.random() * 1000 + 100).toFixed(2))
|
||||||
|
},
|
||||||
|
change_percent: changePercent, // 兼容字段
|
||||||
|
happened_times: generateHappenedTimes(i),
|
||||||
|
stocks: relatedStocks,
|
||||||
|
hot_score: Math.floor(Math.random() * 100)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
233
src/views/StockOverview/components/ConceptStocksModal.tsx
Normal file
233
src/views/StockOverview/components/ConceptStocksModal.tsx
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
Thead,
|
||||||
|
Tbody,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Td,
|
||||||
|
TableContainer,
|
||||||
|
Box,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Icon,
|
||||||
|
Spinner,
|
||||||
|
useColorModeValue,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { FaTable } from 'react-icons/fa';
|
||||||
|
import marketService from '@services/marketService';
|
||||||
|
import { logger } from '@utils/logger';
|
||||||
|
|
||||||
|
// 股票信息类型
|
||||||
|
interface StockInfo {
|
||||||
|
stock_code: string;
|
||||||
|
stock_name: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 概念信息类型
|
||||||
|
export interface ConceptInfo {
|
||||||
|
concept_id?: string;
|
||||||
|
concept_name: string;
|
||||||
|
stock_count?: number;
|
||||||
|
stocks?: StockInfo[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 行情数据类型
|
||||||
|
interface MarketData {
|
||||||
|
stock_code: string;
|
||||||
|
close?: number;
|
||||||
|
change_percent?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConceptStocksModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
concept: ConceptInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConceptStocksModal: React.FC<ConceptStocksModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
concept,
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const [stockMarketData, setStockMarketData] = useState<Record<string, MarketData>>({});
|
||||||
|
const [loadingStockData, setLoadingStockData] = useState(false);
|
||||||
|
|
||||||
|
// 颜色主题
|
||||||
|
const cardBg = useColorModeValue('white', '#1a1a1a');
|
||||||
|
const hoverBg = useColorModeValue('gray.50', '#2a2a2a');
|
||||||
|
|
||||||
|
// 批量获取股票行情数据
|
||||||
|
const fetchStockMarketData = useCallback(async (stocks: StockInfo[]) => {
|
||||||
|
if (!stocks || stocks.length === 0) return;
|
||||||
|
|
||||||
|
setLoadingStockData(true);
|
||||||
|
const newMarketData: Record<string, MarketData> = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const batchSize = 5;
|
||||||
|
for (let i = 0; i < stocks.length; i += batchSize) {
|
||||||
|
const batch = stocks.slice(i, i + batchSize);
|
||||||
|
const promises = batch.map(async (stock) => {
|
||||||
|
if (!stock.stock_code) return null;
|
||||||
|
const seccode = stock.stock_code.substring(0, 6);
|
||||||
|
try {
|
||||||
|
const response = await marketService.getTradeData(seccode, 1);
|
||||||
|
if (response.success && response.data?.length > 0) {
|
||||||
|
const latestData = response.data[response.data.length - 1];
|
||||||
|
return { stock_code: stock.stock_code, ...latestData };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('ConceptStocksModal', '获取股票行情失败', { stockCode: seccode });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
results.forEach((result) => {
|
||||||
|
if (result) newMarketData[result.stock_code] = result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setStockMarketData(newMarketData);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('ConceptStocksModal', 'fetchStockMarketData', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingStockData(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 弹窗打开时加载数据
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isOpen && concept?.stocks) {
|
||||||
|
setStockMarketData({});
|
||||||
|
fetchStockMarketData(concept.stocks);
|
||||||
|
}
|
||||||
|
}, [isOpen, concept, fetchStockMarketData]);
|
||||||
|
|
||||||
|
// 点击股票行
|
||||||
|
const handleStockClick = (stockCode: string) => {
|
||||||
|
navigate(`/company?scode=${stockCode}`);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stocks = concept?.stocks || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
size="4xl"
|
||||||
|
scrollBehavior="inside"
|
||||||
|
>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent bg={cardBg}>
|
||||||
|
<ModalHeader bg="purple.500" color="white" borderTopRadius="md">
|
||||||
|
<HStack>
|
||||||
|
<Icon as={FaTable} />
|
||||||
|
<Text>{concept?.concept_name} - 相关个股</Text>
|
||||||
|
</HStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton color="white" />
|
||||||
|
|
||||||
|
<ModalBody py={6}>
|
||||||
|
{stocks.length === 0 ? (
|
||||||
|
<Text color="gray.500" textAlign="center">暂无相关股票数据</Text>
|
||||||
|
) : (
|
||||||
|
<Box>
|
||||||
|
{loadingStockData && (
|
||||||
|
<HStack justify="center" mb={4}>
|
||||||
|
<Spinner size="sm" color="purple.500" />
|
||||||
|
<Text fontSize="sm" color="gray.500">正在获取行情数据...</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TableContainer maxH="60vh" overflowY="auto">
|
||||||
|
<Table variant="simple" size="sm">
|
||||||
|
<Thead position="sticky" top={0} bg={cardBg} zIndex={1}>
|
||||||
|
<Tr>
|
||||||
|
<Th>股票名称</Th>
|
||||||
|
<Th>股票代码</Th>
|
||||||
|
<Th isNumeric>现价</Th>
|
||||||
|
<Th isNumeric>涨跌幅</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{stocks.map((stock, idx) => {
|
||||||
|
const marketData = stockMarketData[stock.stock_code];
|
||||||
|
const changePercent = marketData?.change_percent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr
|
||||||
|
key={idx}
|
||||||
|
_hover={{ bg: hoverBg }}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => handleStockClick(stock.stock_code)}
|
||||||
|
>
|
||||||
|
<Td color="blue.500" fontWeight="medium">
|
||||||
|
{stock.stock_name}
|
||||||
|
</Td>
|
||||||
|
<Td>{stock.stock_code}</Td>
|
||||||
|
<Td isNumeric>
|
||||||
|
{loadingStockData ? (
|
||||||
|
<Spinner size="xs" />
|
||||||
|
) : marketData?.close ? (
|
||||||
|
`¥${marketData.close.toFixed(2)}`
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td
|
||||||
|
isNumeric
|
||||||
|
fontWeight="bold"
|
||||||
|
color={
|
||||||
|
changePercent && changePercent > 0
|
||||||
|
? 'red.500'
|
||||||
|
: changePercent && changePercent < 0
|
||||||
|
? 'green.500'
|
||||||
|
: 'gray.500'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loadingStockData ? (
|
||||||
|
<Spinner size="xs" />
|
||||||
|
) : changePercent !== undefined ? (
|
||||||
|
`${changePercent > 0 ? '+' : ''}${changePercent.toFixed(2)}%`
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button colorScheme="purple" onClick={onClose}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConceptStocksModal;
|
||||||
@@ -56,6 +56,7 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { SearchIcon, CloseIcon, ArrowForwardIcon, TrendingUpIcon, InfoIcon, ChevronRightIcon, MoonIcon, SunIcon, CalendarIcon } from '@chakra-ui/icons';
|
import { SearchIcon, CloseIcon, ArrowForwardIcon, TrendingUpIcon, InfoIcon, ChevronRightIcon, MoonIcon, SunIcon, CalendarIcon } from '@chakra-ui/icons';
|
||||||
import { FaChartLine, FaFire, FaRocket, FaBrain, FaCalendarAlt, FaChevronRight, FaArrowUp, FaArrowDown, FaChartBar } from 'react-icons/fa';
|
import { FaChartLine, FaFire, FaRocket, FaBrain, FaCalendarAlt, FaChevronRight, FaArrowUp, FaArrowDown, FaChartBar } from 'react-icons/fa';
|
||||||
|
import ConceptStocksModal from './components/ConceptStocksModal';
|
||||||
import { BsGraphUp, BsLightningFill } from 'react-icons/bs';
|
import { BsGraphUp, BsLightningFill } from 'react-icons/bs';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
@@ -102,6 +103,10 @@ const StockOverview = () => {
|
|||||||
const [availableDates, setAvailableDates] = useState([]);
|
const [availableDates, setAvailableDates] = useState([]);
|
||||||
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
|
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
|
||||||
|
|
||||||
|
// 个股列表弹窗状态
|
||||||
|
const [isStockModalOpen, setIsStockModalOpen] = useState(false);
|
||||||
|
const [selectedConcept, setSelectedConcept] = useState(null);
|
||||||
|
|
||||||
// 专业的颜色主题
|
// 专业的颜色主题
|
||||||
const bgColor = useColorModeValue('white', '#0a0a0a');
|
const bgColor = useColorModeValue('white', '#0a0a0a');
|
||||||
const cardBg = useColorModeValue('white', '#1a1a1a');
|
const cardBg = useColorModeValue('white', '#1a1a1a');
|
||||||
@@ -114,6 +119,13 @@ const StockOverview = () => {
|
|||||||
const accentColor = useColorModeValue('purple.600', goldColor);
|
const accentColor = useColorModeValue('purple.600', goldColor);
|
||||||
const heroBg = useColorModeValue('linear(to-br, purple.600, pink.500)', 'linear(to-br, #0a0a0a, #1a1a1a)');
|
const heroBg = useColorModeValue('linear(to-br, purple.600, pink.500)', 'linear(to-br, #0a0a0a, #1a1a1a)');
|
||||||
|
|
||||||
|
// 打开个股列表弹窗
|
||||||
|
const handleViewStocks = useCallback((e, concept) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedConcept(concept);
|
||||||
|
setIsStockModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 防抖搜索
|
// 防抖搜索
|
||||||
const debounceSearch = useCallback(
|
const debounceSearch = useCallback(
|
||||||
(() => {
|
(() => {
|
||||||
@@ -996,37 +1008,39 @@ const StockOverview = () => {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Box w="100%">
|
<Box
|
||||||
<Text fontSize="xs" color="gray.500" mb={2}>
|
w="100%"
|
||||||
包含 {concept.stock_count} 只个股
|
cursor="pointer"
|
||||||
</Text>
|
onClick={(e) => handleViewStocks(e, concept)}
|
||||||
|
_hover={{ bg: hoverBg }}
|
||||||
|
p={2}
|
||||||
|
borderRadius="md"
|
||||||
|
transition="background 0.2s"
|
||||||
|
>
|
||||||
|
<Text fontSize="xs" color="gray.500" mb={2}>
|
||||||
|
包含 {concept.stock_count} 只个股
|
||||||
|
</Text>
|
||||||
|
|
||||||
{concept.stocks && concept.stocks.length > 0 && (
|
{concept.stocks && concept.stocks.length > 0 && (
|
||||||
<Flex flexWrap="wrap" gap={2}>
|
<Flex
|
||||||
{concept.stocks.map((stock, idx) => (
|
flexWrap="nowrap"
|
||||||
<Tag
|
gap={2}
|
||||||
key={idx}
|
overflow="hidden"
|
||||||
size="sm"
|
maxH="24px"
|
||||||
colorScheme="purple"
|
>
|
||||||
variant="subtle"
|
{concept.stocks.map((stock, idx) => (
|
||||||
cursor="pointer"
|
<Tag
|
||||||
onClick={(e) => {
|
key={idx}
|
||||||
e.stopPropagation();
|
size="sm"
|
||||||
|
colorScheme="purple"
|
||||||
// 🎯 追踪概念下的股票标签点击
|
variant="subtle"
|
||||||
trackConceptStockClicked({
|
flexShrink={0}
|
||||||
code: stock.stock_code,
|
>
|
||||||
name: stock.stock_name
|
<TagLabel>{stock.stock_name}</TagLabel>
|
||||||
}, concept.concept_name);
|
</Tag>
|
||||||
|
))}
|
||||||
navigate(`/company?scode=${stock.stock_code}`);
|
</Flex>
|
||||||
}}
|
)}
|
||||||
>
|
|
||||||
<TagLabel>{stock.stock_name}</TagLabel>
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<HStack spacing={2} w="100%">
|
<HStack spacing={2} w="100%">
|
||||||
@@ -1120,7 +1134,14 @@ const StockOverview = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
{/* 个股列表弹窗 */}
|
||||||
|
<ConceptStocksModal
|
||||||
|
isOpen={isStockModalOpen}
|
||||||
|
onClose={() => setIsStockModalOpen(false)}
|
||||||
|
concept={selectedConcept}
|
||||||
|
/>
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user