214 lines
5.8 KiB
TypeScript
214 lines
5.8 KiB
TypeScript
// src/components/StockChart/StockChartModal.tsx - 统一的股票图表组件(KLineChart 实现)
|
||
import React, { useState } from 'react';
|
||
import {
|
||
Modal,
|
||
ModalOverlay,
|
||
ModalContent,
|
||
ModalHeader,
|
||
ModalCloseButton,
|
||
ModalBody,
|
||
Button,
|
||
ButtonGroup,
|
||
VStack,
|
||
HStack,
|
||
Text,
|
||
Badge,
|
||
Box,
|
||
Flex,
|
||
CircularProgress,
|
||
} from '@chakra-ui/react';
|
||
import RiskDisclaimer from '../RiskDisclaimer';
|
||
import { RelationDescription } from '../StockRelation';
|
||
import type { RelationDescType } from '../StockRelation';
|
||
import { useKLineChart, useKLineData, useEventMarker } from './hooks';
|
||
import { Alert, AlertIcon } from '@chakra-ui/react';
|
||
|
||
/**
|
||
* 图表类型
|
||
*/
|
||
type ChartType = 'timeline' | 'daily';
|
||
|
||
/**
|
||
* 股票信息
|
||
*/
|
||
interface StockInfo {
|
||
stock_code: string;
|
||
stock_name?: string;
|
||
relation_desc?: RelationDescType;
|
||
}
|
||
|
||
/**
|
||
* StockChartModal 组件 Props
|
||
*/
|
||
export interface StockChartModalProps {
|
||
/** 模态框是否打开 */
|
||
isOpen: boolean;
|
||
|
||
/** 关闭回调 */
|
||
onClose: () => void;
|
||
|
||
/** 股票信息 */
|
||
stock: StockInfo | null;
|
||
|
||
/** 事件时间 */
|
||
eventTime?: string | null;
|
||
|
||
/** 是否使用 Chakra UI(保留字段,当前未使用) */
|
||
isChakraUI?: boolean;
|
||
|
||
/** 模态框大小 */
|
||
size?: string;
|
||
|
||
/** 初始图表类型 */
|
||
initialChartType?: ChartType;
|
||
}
|
||
|
||
|
||
const StockChartModal: React.FC<StockChartModalProps> = ({
|
||
isOpen,
|
||
onClose,
|
||
stock,
|
||
eventTime,
|
||
isChakraUI = true,
|
||
size = '6xl',
|
||
initialChartType = 'timeline',
|
||
}) => {
|
||
// 状态管理
|
||
const [chartType, setChartType] = useState<ChartType>(initialChartType);
|
||
|
||
// KLineChart Hooks
|
||
const { chart, chartRef, isInitialized, error: chartError } = useKLineChart({
|
||
containerId: `kline-chart-${stock?.stock_code || 'default'}`,
|
||
height: 500,
|
||
autoResize: true,
|
||
chartType, // ✅ 传递 chartType,让 Hook 根据类型应用不同样式
|
||
});
|
||
|
||
const { data, loading, error: dataError } = useKLineData({
|
||
chart,
|
||
stockCode: stock?.stock_code || '',
|
||
chartType,
|
||
eventTime: eventTime || undefined,
|
||
autoLoad: true, // 改为 true,让 Hook 内部根据 stockCode 和 chart 判断是否加载
|
||
});
|
||
|
||
const { marker } = useEventMarker({
|
||
chart,
|
||
data,
|
||
eventTime: eventTime || undefined,
|
||
eventTitle: '事件发生',
|
||
autoCreate: true,
|
||
});
|
||
|
||
// 守卫子句
|
||
if (!stock) return null;
|
||
|
||
return (
|
||
<Modal isOpen={isOpen} onClose={onClose} size={size}>
|
||
<ModalOverlay />
|
||
<ModalContent maxW="90vw" maxH="90vh" overflow="hidden">
|
||
<ModalHeader pb={4} position="relative">
|
||
<VStack align="flex-start" spacing={2}>
|
||
<HStack>
|
||
<Text fontSize="lg" fontWeight="bold">
|
||
{stock.stock_name || stock.stock_code} ({stock.stock_code}) - 股票详情
|
||
</Text>
|
||
{data.length > 0 && <Badge colorScheme="blue">数据点: {data.length}</Badge>}
|
||
</HStack>
|
||
<ButtonGroup size="sm">
|
||
<Button
|
||
variant={chartType === 'timeline' ? 'solid' : 'outline'}
|
||
onClick={() => setChartType('timeline')}
|
||
colorScheme="blue"
|
||
>
|
||
分时线
|
||
</Button>
|
||
<Button
|
||
variant={chartType === 'daily' ? 'solid' : 'outline'}
|
||
onClick={() => setChartType('daily')}
|
||
colorScheme="blue"
|
||
>
|
||
日K线
|
||
</Button>
|
||
</ButtonGroup>
|
||
</VStack>
|
||
|
||
{/* 重件发生标签 - 仅在有 eventTime 时显示 */}
|
||
{eventTime && (
|
||
<Badge
|
||
colorScheme="yellow"
|
||
fontSize="sm"
|
||
px={3}
|
||
py={1}
|
||
borderRadius="md"
|
||
position="absolute"
|
||
top="4"
|
||
right="12"
|
||
boxShadow="sm"
|
||
>
|
||
重件发生(影响日)
|
||
</Badge>
|
||
)}
|
||
</ModalHeader>
|
||
<ModalCloseButton />
|
||
<ModalBody p={0} overflowY="auto" maxH="calc(90vh - 120px)">
|
||
{/* 错误提示 */}
|
||
{(chartError || dataError) && (
|
||
<Alert status="error" mx={4} mt={4}>
|
||
<AlertIcon />
|
||
图表加载失败:{chartError?.message || dataError?.message}
|
||
</Alert>
|
||
)}
|
||
|
||
{/* 图表区域 - 响应式高度 */}
|
||
<Box
|
||
h={{
|
||
base: "calc(60vh - 100px)", // 移动端:60% 视口高度 - 100px
|
||
md: "calc(70vh - 150px)", // 平板:70% 视口高度 - 150px
|
||
lg: "calc(80vh - 200px)" // 桌面:80% 视口高度 - 200px
|
||
}}
|
||
minH="350px" // 最小高度:确保可用性
|
||
maxH="650px" // 最大高度:避免过大
|
||
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}
|
||
id={`kline-chart-${stock.stock_code}`}
|
||
style={{ width: '100%', height: '100%' }}
|
||
/>
|
||
</Box>
|
||
|
||
{/* 关联描述 */}
|
||
<RelationDescription relationDesc={stock?.relation_desc} />
|
||
|
||
{/* 风险提示 */}
|
||
<Box px={4} pb={4}>
|
||
<RiskDisclaimer text="" variant="default" sx={{}} />
|
||
</Box>
|
||
</ModalBody>
|
||
</ModalContent>
|
||
</Modal>
|
||
);
|
||
};
|
||
|
||
export default StockChartModal;
|