Files
vf_react/src/views/EventDetail/components/RelatedStocks.js
2025-10-11 12:10:00 +08:00

953 lines
39 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/views/EventDetail/components/RelatedStocks.js - 完整修改版本
import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
Box,
HStack,
VStack,
Text,
Button,
Badge,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
ModalFooter,
useDisclosure,
Skeleton,
Alert,
AlertIcon,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Input,
Textarea,
FormControl,
FormLabel,
ButtonGroup,
IconButton,
Tooltip,
useColorModeValue,
CircularProgress,
CircularProgressLabel,
Flex,
Spacer,
useToast
} from '@chakra-ui/react';
import {
FaPlus,
FaTrash,
FaChartLine,
FaRedo,
FaSearch
} from 'react-icons/fa';
import * as echarts from 'echarts';
import { eventService, stockService } from '../../../services/eventService';
const RelatedStocks = ({
eventId,
eventTime, // 新增:从父组件传递事件时间
stocks = [],
loading = false,
error = null,
onStockAdded = () => {},
onStockDeleted = () => {}
}) => {
// ==================== 状态管理 ====================
const [stocksData, setStocksData] = useState(stocks);
const [quotes, setQuotes] = useState({});
const [quotesLoading, setQuotesLoading] = useState(false);
const [selectedStock, setSelectedStock] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [sortField, setSortField] = useState('correlation');
const [sortOrder, setSortOrder] = useState('desc');
// 模态框状态
const {
isOpen: isAddModalOpen,
onOpen: onAddModalOpen,
onClose: onAddModalClose
} = useDisclosure();
const {
isOpen: isChartModalOpen,
onOpen: onChartModalOpen,
onClose: onChartModalClose
} = useDisclosure();
// 添加股票表单状态
const [addStockForm, setAddStockForm] = useState({
stock_code: '',
relation_desc: ''
});
// 主题和工具
const toast = useToast();
const tableBg = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
// ==================== 副作用处理 ====================
useEffect(() => {
setStocksData(stocks);
}, [stocks]);
useEffect(() => {
if (stocksData.length > 0) {
fetchQuotes();
}
}, [stocksData, eventTime]); // 添加 eventTime 依赖
// ==================== API 调用函数 ====================
const fetchQuotes = useCallback(async () => {
if (stocksData.length === 0) return;
try {
setQuotesLoading(true);
const codes = stocksData.map(stock => stock.stock_code);
console.log('获取股票报价,代码:', codes, '事件时间:', eventTime);
const response = await stockService.getQuotes(codes, eventTime);
console.log('股票报价响应:', response);
setQuotes(response || {});
} catch (err) {
console.error('获取股票报价失败:', err);
toast({
title: '获取股票报价失败',
description: err.message,
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setQuotesLoading(false);
}
}, [stocksData, eventTime, toast]);
const handleRefreshQuotes = () => {
fetchQuotes();
toast({
title: '正在刷新报价...',
status: 'info',
duration: 2000,
isClosable: true,
});
};
const handleAddStock = async () => {
if (!addStockForm.stock_code.trim() || !addStockForm.relation_desc.trim()) {
toast({
title: '请填写完整信息',
status: 'warning',
duration: 3000,
isClosable: true,
});
return;
}
try {
await eventService.addRelatedStock(eventId, addStockForm);
toast({
title: '添加成功',
status: 'success',
duration: 3000,
isClosable: true,
});
// 重置表单
setAddStockForm({ stock_code: '', relation_desc: '' });
onAddModalClose();
onStockAdded();
} catch (err) {
toast({
title: '添加失败',
description: err.message,
status: 'error',
duration: 5000,
isClosable: true,
});
}
};
const handleDeleteStock = async (stockId, stockCode) => {
if (!window.confirm(`确定要删除股票 ${stockCode} 吗?`)) {
return;
}
try {
await eventService.deleteRelatedStock(stockId);
toast({
title: '删除成功',
status: 'success',
duration: 3000,
isClosable: true,
});
onStockDeleted();
} catch (err) {
toast({
title: '删除失败',
description: err.message,
status: 'error',
duration: 5000,
isClosable: true,
});
}
};
const handleShowChart = (stock) => {
setSelectedStock(stock);
onChartModalOpen();
};
// ==================== 辅助函数 ====================
const getCorrelationColor = (correlation) => {
const value = (correlation || 0) * 100;
if (value >= 80) return 'red';
if (value >= 60) return 'orange';
return 'green';
};
const formatPriceChange = (change) => {
if (!change && change !== 0) return '--';
const prefix = change > 0 ? '+' : '';
return `${prefix}${change.toFixed(2)}%`;
};
const getStockName = (stock) => {
// 优先使用API返回的名称
const quote = quotes[stock.stock_code];
if (quote && quote.name) {
return quote.name;
}
// 其次使用数据库中的名称
if (stock.stock_name) {
return stock.stock_name;
}
// 最后使用默认格式
return `股票${stock.stock_code.split('.')[0]}`;
};
// ==================== 数据处理 ====================
const filteredAndSortedStocks = stocksData
.filter(stock => {
const stockName = getStockName(stock);
return stock.stock_code.toLowerCase().includes(searchTerm.toLowerCase()) ||
stockName.toLowerCase().includes(searchTerm.toLowerCase());
})
.sort((a, b) => {
let aValue, bValue;
switch (sortField) {
case 'correlation':
aValue = a.correlation || 0;
bValue = b.correlation || 0;
break;
case 'change':
aValue = quotes[a.stock_code]?.change || 0;
bValue = quotes[b.stock_code]?.change || 0;
break;
case 'price':
aValue = quotes[a.stock_code]?.price || 0;
bValue = quotes[b.stock_code]?.price || 0;
break;
default:
aValue = a[sortField] || '';
bValue = b[sortField] || '';
}
if (sortOrder === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
// ==================== 渲染状态 ====================
if (loading) {
return (
<VStack spacing={4}>
{[1, 2, 3].map((i) => (
<HStack key={i} w="100%" spacing={4}>
<Skeleton height="20px" width="100px" />
<Skeleton height="20px" width="150px" />
<Skeleton height="20px" width="80px" />
<Skeleton height="20px" width="100px" />
<Skeleton height="20px" width="60px" />
<Skeleton height="20px" width="120px" />
</HStack>
))}
</VStack>
);
}
if (error) {
return (
<Alert status="error" borderRadius="lg">
<AlertIcon />
加载相关股票失败: {error}
</Alert>
);
}
// ==================== 主要渲染 ====================
return (
<>
{/* 工具栏 */}
<Flex mb={4} align="center" wrap="wrap" gap={4}>
{/* 搜索框 */}
<HStack>
<FaSearch />
<Input
placeholder="搜索股票代码或名称..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
size="sm"
maxW="200px"
/>
</HStack>
<Spacer />
{/* 操作按钮 */}
<HStack>
<Tooltip label="刷新报价">
<IconButton
icon={<FaRedo />}
size="sm"
onClick={handleRefreshQuotes}
isLoading={quotesLoading}
aria-label="刷新报价"
/>
</Tooltip>
<Button
leftIcon={<FaPlus />}
size="sm"
colorScheme="blue"
onClick={onAddModalOpen}
>
添加股票
</Button>
</HStack>
</Flex>
{/* 股票表格 */}
<Box bg={tableBg} borderRadius="lg" overflow="hidden" border="1px solid" borderColor={borderColor}>
<TableContainer>
<Table size="sm">
<Thead>
<Tr>
<Th cursor="pointer" onClick={() => {
if (sortField === 'stock_code') {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortField('stock_code');
setSortOrder('asc');
}
}}>
股票代码 {sortField === 'stock_code' && (sortOrder === 'asc' ? '↑' : '↓')}
</Th>
<Th>股票名称</Th>
<Th>产业链</Th>
<Th isNumeric cursor="pointer" onClick={() => {
if (sortField === 'price') {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortField('price');
setSortOrder('desc');
}
}}>
最新价 {sortField === 'price' && (sortOrder === 'asc' ? '↑' : '↓')}
</Th>
<Th isNumeric cursor="pointer" onClick={() => {
if (sortField === 'change') {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortField('change');
setSortOrder('desc');
}
}}>
涨跌幅 {sortField === 'change' && (sortOrder === 'asc' ? '↑' : '↓')}
</Th>
<Th textAlign="center">分时图</Th>
<Th textAlign="center" cursor="pointer" onClick={() => {
if (sortField === 'correlation') {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortField('correlation');
setSortOrder('desc');
}
}}>
相关度 {sortField === 'correlation' && (sortOrder === 'asc' ? '↑' : '↓')}
</Th>
<Th>关联描述</Th>
<Th>操作</Th>
</Tr>
</Thead>
<Tbody>
{filteredAndSortedStocks.length === 0 ? (
<Tr>
<Td colSpan={9} textAlign="center" py={8} color="gray.500">
{searchTerm ? '未找到匹配的股票' : '暂无相关股票数据'}
</Td>
</Tr>
) : (
filteredAndSortedStocks.map((stock) => {
const quote = quotes[stock.stock_code] || {};
const correlation = (stock.correlation || 0) * 100;
const stockName = getStockName(stock);
return (
<Tr key={stock.id} _hover={{ bg: useColorModeValue('gray.50', 'gray.700') }}>
{/* 股票代码 */}
<Td>
<Button
variant="link"
color="blue.500"
fontFamily="mono"
fontWeight="bold"
p={0}
onClick={() => handleShowChart(stock)}
>
{stock.stock_code}
</Button>
</Td>
{/* 股票名称 */}
<Td>
<VStack align="flex-start" spacing={0}>
<Text fontSize="sm" fontWeight="medium">
{stockName}
</Text>
{quotesLoading && (
<Text fontSize="xs" color="gray.500">
加载中...
</Text>
)}
</VStack>
</Td>
{/* 产业链 */}
<Td>
<Badge size="sm" variant="outline">
{stock.sector || '未知'}
</Badge>
</Td>
{/* 最新价 */}
<Td isNumeric>
<Text fontSize="sm" fontWeight="medium">
{quote.price ? `¥${quote.price.toFixed(2)}` : '--'}
</Text>
</Td>
{/* 涨跌幅 */}
<Td isNumeric>
<Badge
colorScheme={
!quote.change ? 'gray' :
quote.change > 0 ? 'red' :
quote.change < 0 ? 'green' : 'gray'
}
size="sm"
>
{formatPriceChange(quote.change)}
</Badge>
</Td>
{/* 分时图 */}
<Td>
<Box width="120px" height="40px">
<MiniChart
stockCode={stock.stock_code}
eventTime={eventTime}
onChartClick={() => handleShowChart(stock)}
/>
</Box>
</Td>
{/* 相关度 */}
<Td textAlign="center">
<CircularProgress
value={correlation}
color={getCorrelationColor(stock.correlation)}
size="50px"
>
<CircularProgressLabel fontSize="xs">
{Math.round(correlation)}%
</CircularProgressLabel>
</CircularProgress>
</Td>
{/* 关联描述 */}
<Td maxW="200px">
<Text fontSize="xs" noOfLines={2}>
{stock.relation_desc || '--'}
</Text>
</Td>
{/* 操作 */}
<Td>
<HStack spacing={1}>
<Tooltip label="查看K线图">
<IconButton
icon={<FaChartLine />}
size="xs"
onClick={() => handleShowChart(stock)}
aria-label="查看K线图"
/>
</Tooltip>
<Tooltip label="删除">
<IconButton
icon={<FaTrash />}
size="xs"
colorScheme="red"
variant="ghost"
onClick={() => handleDeleteStock(stock.id, stock.stock_code)}
aria-label="删除股票"
/>
</Tooltip>
</HStack>
</Td>
</Tr>
);
})
)}
</Tbody>
</Table>
</TableContainer>
</Box>
{/* 添加股票模态框 */}
<AddStockModal
isOpen={isAddModalOpen}
onClose={onAddModalClose}
formData={addStockForm}
setFormData={setAddStockForm}
onSubmit={handleAddStock}
/>
{/* 股票图表模态框 */}
<StockChartModal
isOpen={isChartModalOpen}
onClose={onChartModalClose}
stock={selectedStock}
eventTime={eventTime}
/>
</>
);
};
// ==================== 子组件 ====================
// 迷你分时图组件
const MiniChart = ({ stockCode, eventTime, onChartClick }) => {
const chartRef = useRef(null);
const chartInstanceRef = useRef(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (chartRef.current && stockCode) {
loadChartData();
}
return () => {
if (chartInstanceRef.current) {
chartInstanceRef.current.dispose();
chartInstanceRef.current = null;
}
};
}, [stockCode, eventTime]);
const loadChartData = async () => {
try {
setLoading(true);
setError(null);
const response = await stockService.getKlineData(stockCode, 'timeline', eventTime);
if (!response.data || response.data.length === 0) {
setError('无数据');
return;
}
// 初始化图表
if (!chartInstanceRef.current && chartRef.current) {
chartInstanceRef.current = echarts.init(chartRef.current);
}
const option = generateMiniChartOption(response.data);
chartInstanceRef.current.setOption(option, true);
} catch (err) {
console.error('加载迷你图表失败:', err);
setError('加载失败');
} finally {
setLoading(false);
}
};
const generateMiniChartOption = (data) => {
const prices = data.map(item => item.close);
const times = data.map(item => item.time);
// 计算最高最低价格
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
// 判断是上涨还是下跌
const isUp = prices[prices.length - 1] >= prices[0];
const lineColor = isUp ? '#ef5350' : '#26a69a';
return {
grid: {
left: 2,
right: 2,
top: 2,
bottom: 2,
containLabel: false
},
xAxis: {
type: 'category',
data: times,
show: false,
boundaryGap: false
},
yAxis: {
type: 'value',
show: false,
min: minPrice * 0.995,
max: maxPrice * 1.005
},
series: [{
data: prices,
type: 'line',
smooth: true,
symbol: 'none',
lineStyle: {
color: lineColor,
width: 2
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: lineColor === '#ef5350' ? 'rgba(239, 83, 80, 0.3)' : 'rgba(38, 166, 154, 0.3)'
},
{
offset: 1,
color: lineColor === '#ef5350' ? 'rgba(239, 83, 80, 0.05)' : 'rgba(38, 166, 154, 0.05)'
}
])
},
markLine: {
silent: true,
symbol: 'none',
label: { show: false },
lineStyle: {
color: '#aaa',
type: 'dashed',
width: 1
},
data: [{
yAxis: prices[0] // 参考价
}]
}
}],
tooltip: {
trigger: 'axis',
formatter: function(params) {
if (!params || params.length === 0) return '';
const price = params[0].value.toFixed(2);
const time = params[0].axisValue;
const percentChange = ((price - prices[0]) / prices[0] * 100).toFixed(2);
const sign = percentChange >= 0 ? '+' : '';
return `${time}<br/>价格: ${price}<br/>变动: ${sign}${percentChange}%`;
},
position: function (pos, params, el, elRect, size) {
const obj = { top: 10 };
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 30;
return obj;
}
},
animation: false
};
};
if (loading) {
return (
<Flex align="center" justify="center" h="100%" w="100%">
<Text fontSize="xs" color="gray.500">加载中...</Text>
</Flex>
);
}
if (error) {
return (
<Flex align="center" justify="center" h="100%" w="100%">
<Text fontSize="xs" color="gray.500">{error}</Text>
</Flex>
);
}
return (
<Box
ref={chartRef}
w="100%"
h="100%"
cursor="pointer"
onClick={onChartClick}
_hover={{ opacity: 0.8 }}
/>
);
};
// 添加股票模态框组件
const AddStockModal = ({ isOpen, onClose, formData, setFormData, onSubmit }) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>添加相关股票</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={4}>
<FormControl isRequired>
<FormLabel>股票代码</FormLabel>
<Input
placeholder="请输入股票代码000001.SZ"
value={formData.stock_code}
onChange={(e) => setFormData(prev => ({
...prev,
stock_code: e.target.value.toUpperCase()
}))}
/>
</FormControl>
<FormControl isRequired>
<FormLabel>关联描述</FormLabel>
<Textarea
placeholder="请描述该股票与事件的关联原因..."
value={formData.relation_desc}
onChange={(e) => setFormData(prev => ({
...prev,
relation_desc: e.target.value
}))}
rows={4}
/>
</FormControl>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onClose}>
取消
</Button>
<Button colorScheme="blue" onClick={onSubmit}>
确定
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
// 股票图表模态框组件
const StockChartModal = ({ isOpen, onClose, stock, eventTime }) => {
const chartRef = useRef(null);
const chartInstanceRef = useRef(null);
const [chartType, setChartType] = useState('timeline');
const [loading, setLoading] = useState(false);
const [chartData, setChartData] = useState(null);
const toast = useToast();
useEffect(() => {
if (isOpen && chartRef.current) {
loadChartData(chartType);
}
return () => {
if (chartInstanceRef.current) {
window.removeEventListener('resize', chartInstanceRef.current.resizeHandler);
chartInstanceRef.current.dispose();
chartInstanceRef.current = null;
}
};
}, [isOpen]);
useEffect(() => {
if (isOpen && chartRef.current) {
loadChartData(chartType);
}
}, [chartType, stock, eventTime]);
const loadChartData = async (type) => {
if (!stock || !chartRef.current) return;
try {
setLoading(true);
if (chartInstanceRef.current) {
chartInstanceRef.current.showLoading();
}
const response = await stockService.getKlineData(stock.stock_code, type, eventTime);
setChartData(response);
if (!chartRef.current) return;
if (!chartInstanceRef.current) {
const chart = echarts.init(chartRef.current);
chart.resizeHandler = () => chart.resize();
window.addEventListener('resize', chart.resizeHandler);
chartInstanceRef.current = chart;
}
chartInstanceRef.current.hideLoading();
const option = generateChartOption(response, type);
chartInstanceRef.current.setOption(option, true);
} catch (err) {
console.error('加载图表数据失败:', err);
toast({ title: '加载图表数据失败', description: err.message, status: 'error', duration: 3000, isClosable: true, });
if (chartInstanceRef.current) {
chartInstanceRef.current.hideLoading();
}
} finally {
setLoading(false);
}
};
const generateChartOption = (data, type) => {
if (!data || !data.data || data.data.length === 0) {
return { title: { text: '暂无数据', left: 'center', top: 'center', textStyle: { color: '#999', fontSize: 16 } } };
}
const stockData = data.data;
if (type === 'timeline' || type === 'minute') {
const times = stockData.map(item => item.time);
const prices = stockData.map(item => item.close);
const isUp = prices[prices.length - 1] >= prices[0];
const lineColor = isUp ? '#ef5350' : '#26a69a';
return {
title: { text: `${data.name} (${data.code}) - ${type === 'timeline' ? '分时图' : '分钟线'}`, left: 'center', textStyle: { fontSize: 16, fontWeight: 'bold' } },
tooltip: {
trigger: 'axis',
formatter: function(params) {
if (!params || params.length === 0) {
return '';
}
const point = params[0];
if (!point) return '';
return `时间: ${point.axisValue}<br/>价格: ¥${point.value.toFixed(2)}`;
}
},
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: times, boundaryGap: false },
yAxis: { type: 'value', scale: true },
series: [{ data: prices, type: 'line', smooth: true, symbol: 'none', lineStyle: { color: lineColor, width: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: isUp ? 'rgba(239, 83, 80, 0.3)' : 'rgba(38, 166, 154, 0.3)' }, { offset: 1, color: isUp ? 'rgba(239, 83, 80, 0.1)' : 'rgba(38, 166, 154, 0.1)' }]) } }]
};
}
if (type === 'daily') {
const dates = stockData.map(item => item.time);
const klineData = stockData.map(item => [item.open, item.close, item.low, item.high]);
const volumes = stockData.map(item => item.volume);
return {
title: { text: `${data.name} (${data.code}) - 日K线`, left: 'center', textStyle: { fontSize: 16, fontWeight: 'bold' } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
formatter: function(params) {
if (!params || !Array.isArray(params) || params.length === 0) {
return '';
}
const kline = params[0];
const volume = params[1];
if (!kline || !kline.data) {
return '';
}
let tooltipHtml = `日期: ${kline.axisValue}<br/>
开盘: ${kline.data[0]}<br/>
收盘: ${kline.data[1]}<br/>
最低: ${kline.data[2]}<br/>
最高: ${kline.data[3]}`;
if (volume && typeof volume.value !== 'undefined') {
tooltipHtml += `<br/>成交量: ${volume.value}`;
}
return tooltipHtml;
}
},
grid: [{ left: '10%', right: '10%', height: '60%' }, { left: '10%', right: '10%', top: '70%', height: '16%' }],
xAxis: [{ type: 'category', data: dates, scale: true, boundaryGap: false, axisLine: { onZero: false }, splitLine: { show: false }, min: 'dataMin', max: 'dataMax' }, { type: 'category', gridIndex: 1, data: dates, scale: true, boundaryGap: false, axisLine: { onZero: false }, axisTick: { show: false }, splitLine: { show: false }, axisLabel: { show: false }, min: 'dataMin', max: 'dataMax' }],
yAxis: [{ scale: true, splitArea: { show: true } }, { scale: true, gridIndex: 1, splitNumber: 2, axisLabel: { show: false }, axisLine: { show: false }, axisTick: { show: false }, splitLine: { show: false } }],
dataZoom: [{ type: 'inside', xAxisIndex: [0, 1], start: 80, end: 100 }, { show: true, xAxisIndex: [0, 1], type: 'slider', bottom: 10, start: 80, end: 100 }],
series: [{ name: 'K线', type: 'candlestick', data: klineData, itemStyle: { color: '#ef5350', color0: '#26a69a', borderColor: '#ef5350', borderColor0: '#26a69a' } }, { name: '成交量', type: 'bar', xAxisIndex: 1, yAxisIndex: 1, data: volumes, itemStyle: { color: function(params) { const dataIndex = params.dataIndex; if (dataIndex === 0) return '#ef5350'; return stockData[dataIndex].close >= stockData[dataIndex - 1].close ? '#ef5350' : '#26a69a'; } } }]
};
}
return {};
};
if (!stock) return null;
return (
<Modal isOpen={isOpen} onClose={onClose} size="6xl">
<ModalOverlay />
<ModalContent maxW="90vw" maxH="90vh">
<ModalHeader>
<VStack align="flex-start" spacing={2}>
<HStack>
<Text fontSize="lg" fontWeight="bold">{stock.stock_code} - 股票详情</Text>
{chartData && (<Badge colorScheme="blue">{chartData.trade_date}</Badge>)}
</HStack>
<ButtonGroup size="sm">
<Button variant={chartType === 'timeline' ? 'solid' : 'outline'} onClick={() => setChartType('timeline')} colorScheme="blue">分时图</Button>
<Button variant={chartType === 'minute' ? 'solid' : 'outline'} onClick={() => setChartType('minute')} colorScheme="blue">分钟线</Button>
<Button variant={chartType === 'daily' ? 'solid' : 'outline'} onClick={() => setChartType('daily')} colorScheme="blue">日K线</Button>
</ButtonGroup>
</VStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody p={0}>
<Box h="500px" w="100%" position="relative">
{loading && (
<Flex position="absolute" top="0" left="0" right="0" bottom="0" bg="rgba(255, 255, 255, 0.7)" zIndex="10" alignItems="center" justifyContent="center" >
<VStack spacing={4}>
<CircularProgress isIndeterminate color="blue.300" />
<Text>加载图表数据...</Text>
</VStack>
</Flex>
)}
<div ref={chartRef} style={{ height: '100%', width: '100%', minHeight: '500px' }}/>
</Box>
{stock?.relation_desc && (
<Box p={4} borderTop="1px solid" borderTopColor="gray.200">
<Text fontSize="sm" fontWeight="bold" mb={2}>关联描述:</Text>
<Text fontSize="sm" color="gray.600">{stock.relation_desc}</Text>
</Box>
)}
{process.env.NODE_ENV === 'development' && chartData && (
<Box p={4} bg="gray.50" fontSize="xs" color="gray.600">
<Text fontWeight="bold">调试信息:</Text>
<Text>数据条数: {chartData.data ? chartData.data.length : 0}</Text>
<Text>交易日期: {chartData.trade_date}</Text>
<Text>图表类型: {chartData.type}</Text>
</Box>
)}
</ModalBody>
</ModalContent>
</Modal>
);
};
export default RelatedStocks;