update pay function
This commit is contained in:
427
src/components/StockChart/KLineChartModal.tsx
Normal file
427
src/components/StockChart/KLineChartModal.tsx
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
// src/components/StockChart/KLineChartModal.tsx - K线图弹窗组件
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalBody,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
CircularProgress,
|
||||||
|
Alert,
|
||||||
|
AlertIcon,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
import { stockService } from '@services/eventService';
|
||||||
|
import { logger } from '@utils/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 股票信息
|
||||||
|
*/
|
||||||
|
interface StockInfo {
|
||||||
|
stock_code: string;
|
||||||
|
stock_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KLineChartModal 组件 Props
|
||||||
|
*/
|
||||||
|
export interface KLineChartModalProps {
|
||||||
|
/** 模态框是否打开 */
|
||||||
|
isOpen: boolean;
|
||||||
|
/** 关闭回调 */
|
||||||
|
onClose: () => void;
|
||||||
|
/** 股票信息 */
|
||||||
|
stock: StockInfo | null;
|
||||||
|
/** 事件时间 */
|
||||||
|
eventTime?: string | null;
|
||||||
|
/** 模态框大小 */
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* K线数据点
|
||||||
|
*/
|
||||||
|
interface KLineDataPoint {
|
||||||
|
time: string;
|
||||||
|
open: number;
|
||||||
|
high: number;
|
||||||
|
low: number;
|
||||||
|
close: number;
|
||||||
|
volume: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
stock,
|
||||||
|
eventTime,
|
||||||
|
size = '5xl',
|
||||||
|
}) => {
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
const chartInstance = useRef<echarts.ECharts | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [data, setData] = useState<KLineDataPoint[]>([]);
|
||||||
|
|
||||||
|
// 加载K线数据
|
||||||
|
const loadData = async () => {
|
||||||
|
if (!stock?.stock_code) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug('KLineChartModal', 'loadData', '开始加载K线数据', {
|
||||||
|
stockCode: stock.stock_code,
|
||||||
|
eventTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await stockService.getKlineData(
|
||||||
|
stock.stock_code,
|
||||||
|
'daily',
|
||||||
|
eventTime || undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response || !response.data || response.data.length === 0) {
|
||||||
|
throw new Error('暂无K线数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(response.data);
|
||||||
|
logger.info('KLineChartModal', 'loadData', 'K线数据加载成功', {
|
||||||
|
dataCount: response.data.length,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : '数据加载失败';
|
||||||
|
logger.error('KLineChartModal', 'loadData', err as Error);
|
||||||
|
setError(errorMsg);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartRef.current || !isOpen) return;
|
||||||
|
|
||||||
|
// 创建图表实例
|
||||||
|
chartInstance.current = echarts.init(chartRef.current, 'dark');
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
|
const handleResize = () => {
|
||||||
|
chartInstance.current?.resize();
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
chartInstance.current?.dispose();
|
||||||
|
chartInstance.current = null;
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// 更新图表数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartInstance.current || data.length === 0) return;
|
||||||
|
|
||||||
|
const dates = data.map((d) => d.time);
|
||||||
|
const klineData = data.map((d) => [d.open, d.close, d.low, d.high]);
|
||||||
|
const volumes = data.map((d) => d.volume);
|
||||||
|
|
||||||
|
// 计算成交量柱子颜色(涨为红,跌为绿)
|
||||||
|
const volumeColors = data.map((d) =>
|
||||||
|
d.close >= d.open ? '#ef5350' : '#26a69a'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 图表配置
|
||||||
|
const option: echarts.EChartsOption = {
|
||||||
|
backgroundColor: '#1a1a1a',
|
||||||
|
title: {
|
||||||
|
text: `${stock?.stock_name || stock?.stock_code} - 日K线`,
|
||||||
|
left: 'center',
|
||||||
|
top: 10,
|
||||||
|
textStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: 'rgba(30, 30, 30, 0.95)',
|
||||||
|
borderColor: '#404040',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
crossStyle: {
|
||||||
|
color: '#999',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
formatter: (params: any) => {
|
||||||
|
const dataIndex = params[0]?.dataIndex;
|
||||||
|
if (dataIndex === undefined) return '';
|
||||||
|
|
||||||
|
const item = data[dataIndex];
|
||||||
|
const change = item.close - item.open;
|
||||||
|
const changePercent = (change / item.open) * 100;
|
||||||
|
const changeColor = change >= 0 ? '#ef5350' : '#26a69a';
|
||||||
|
const changeSign = change >= 0 ? '+' : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="padding: 8px;">
|
||||||
|
<div style="font-weight: bold; margin-bottom: 8px;">${item.time}</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||||
|
<span>开盘:</span>
|
||||||
|
<span style="margin-left: 20px;">${item.open.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||||
|
<span>收盘:</span>
|
||||||
|
<span style="color: ${changeColor}; font-weight: bold; margin-left: 20px;">${item.close.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||||
|
<span>最高:</span>
|
||||||
|
<span style="margin-left: 20px;">${item.high.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||||
|
<span>最低:</span>
|
||||||
|
<span style="margin-left: 20px;">${item.low.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||||
|
<span>涨跌额:</span>
|
||||||
|
<span style="color: ${changeColor}; margin-left: 20px;">${changeSign}${change.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||||
|
<span>涨跌幅:</span>
|
||||||
|
<span style="color: ${changeColor}; margin-left: 20px;">${changeSign}${changePercent.toFixed(2)}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
<span>成交量:</span>
|
||||||
|
<span style="margin-left: 20px;">${(item.volume / 100).toFixed(0)}手</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: [
|
||||||
|
{
|
||||||
|
left: '5%',
|
||||||
|
right: '5%',
|
||||||
|
top: '15%',
|
||||||
|
height: '55%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
left: '5%',
|
||||||
|
right: '5%',
|
||||||
|
top: '75%',
|
||||||
|
height: '15%',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
data: dates,
|
||||||
|
gridIndex: 0,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#404040',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#999',
|
||||||
|
interval: Math.floor(dates.length / 8),
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
data: dates,
|
||||||
|
gridIndex: 1,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#404040',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#999',
|
||||||
|
interval: Math.floor(dates.length / 8),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
scale: true,
|
||||||
|
gridIndex: 0,
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#2a2a2a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#404040',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#999',
|
||||||
|
formatter: (value: number) => value.toFixed(2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: true,
|
||||||
|
gridIndex: 1,
|
||||||
|
splitLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#404040',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#999',
|
||||||
|
formatter: (value: number) => {
|
||||||
|
if (value >= 100000000) {
|
||||||
|
return (value / 100000000).toFixed(1) + '亿';
|
||||||
|
} else if (value >= 10000) {
|
||||||
|
return (value / 10000).toFixed(1) + '万';
|
||||||
|
}
|
||||||
|
return value.toFixed(0);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'K线',
|
||||||
|
type: 'candlestick',
|
||||||
|
data: klineData,
|
||||||
|
xAxisIndex: 0,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#ef5350', // 涨
|
||||||
|
color0: '#26a69a', // 跌
|
||||||
|
borderColor: '#ef5350',
|
||||||
|
borderColor0: '#26a69a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '成交量',
|
||||||
|
type: 'bar',
|
||||||
|
data: volumes,
|
||||||
|
xAxisIndex: 1,
|
||||||
|
yAxisIndex: 1,
|
||||||
|
itemStyle: {
|
||||||
|
color: (params: any) => {
|
||||||
|
return volumeColors[params.dataIndex];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'inside',
|
||||||
|
xAxisIndex: [0, 1],
|
||||||
|
start: 50,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'slider',
|
||||||
|
xAxisIndex: [0, 1],
|
||||||
|
start: 50,
|
||||||
|
end: 100,
|
||||||
|
bottom: '2%',
|
||||||
|
height: 20,
|
||||||
|
textStyle: {
|
||||||
|
color: '#999',
|
||||||
|
},
|
||||||
|
borderColor: '#404040',
|
||||||
|
fillerColor: 'rgba(33, 150, 243, 0.2)',
|
||||||
|
handleStyle: {
|
||||||
|
color: '#2196f3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
chartInstance.current.setOption(option);
|
||||||
|
}, [data, stock]);
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
}, [isOpen, stock?.stock_code, eventTime]);
|
||||||
|
|
||||||
|
if (!stock) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size={size}>
|
||||||
|
<ModalOverlay bg="blackAlpha.700" />
|
||||||
|
<ModalContent
|
||||||
|
maxW="90vw"
|
||||||
|
maxH="85vh"
|
||||||
|
bg="#1a1a1a"
|
||||||
|
borderColor="#404040"
|
||||||
|
borderWidth="1px"
|
||||||
|
>
|
||||||
|
<ModalHeader pb={3} borderBottomWidth="1px" borderColor="#404040">
|
||||||
|
<VStack align="flex-start" spacing={1}>
|
||||||
|
<HStack>
|
||||||
|
<Text fontSize="lg" fontWeight="bold" color="#e0e0e0">
|
||||||
|
{stock.stock_name || stock.stock_code} ({stock.stock_code})
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text fontSize="sm" color="#999">
|
||||||
|
日K线图
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton color="#999" _hover={{ color: '#e0e0e0' }} />
|
||||||
|
<ModalBody p={4}>
|
||||||
|
{error && (
|
||||||
|
<Alert status="error" bg="#2a1a1a" borderColor="#ef5350" mb={4}>
|
||||||
|
<AlertIcon color="#ef5350" />
|
||||||
|
<Text color="#e0e0e0">{error}</Text>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box position="relative" h="650px" w="100%">
|
||||||
|
{loading && (
|
||||||
|
<Flex
|
||||||
|
position="absolute"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
right="0"
|
||||||
|
bottom="0"
|
||||||
|
bg="rgba(26, 26, 26, 0.7)"
|
||||||
|
zIndex="10"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<VStack spacing={4}>
|
||||||
|
<CircularProgress isIndeterminate color="blue.400" />
|
||||||
|
<Text color="#e0e0e0">加载K线数据...</Text>
|
||||||
|
</VStack>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
<div ref={chartRef} style={{ width: '100%', height: '100%' }} />
|
||||||
|
</Box>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KLineChartModal;
|
||||||
419
src/components/StockChart/TimelineChartModal.tsx
Normal file
419
src/components/StockChart/TimelineChartModal.tsx
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
// src/components/StockChart/TimelineChartModal.tsx - 分时图弹窗组件
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalBody,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
CircularProgress,
|
||||||
|
Alert,
|
||||||
|
AlertIcon,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
import { stockService } from '@services/eventService';
|
||||||
|
import { logger } from '@utils/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 股票信息
|
||||||
|
*/
|
||||||
|
interface StockInfo {
|
||||||
|
stock_code: string;
|
||||||
|
stock_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimelineChartModal 组件 Props
|
||||||
|
*/
|
||||||
|
export interface TimelineChartModalProps {
|
||||||
|
/** 模态框是否打开 */
|
||||||
|
isOpen: boolean;
|
||||||
|
/** 关闭回调 */
|
||||||
|
onClose: () => void;
|
||||||
|
/** 股票信息 */
|
||||||
|
stock: StockInfo | null;
|
||||||
|
/** 事件时间 */
|
||||||
|
eventTime?: string | null;
|
||||||
|
/** 模态框大小 */
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分时图数据点
|
||||||
|
*/
|
||||||
|
interface TimelineDataPoint {
|
||||||
|
time: string;
|
||||||
|
price: number;
|
||||||
|
avg_price: number;
|
||||||
|
volume: number;
|
||||||
|
change_percent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
stock,
|
||||||
|
eventTime,
|
||||||
|
size = '5xl',
|
||||||
|
}) => {
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
const chartInstance = useRef<echarts.ECharts | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [data, setData] = useState<TimelineDataPoint[]>([]);
|
||||||
|
|
||||||
|
// 加载分时图数据
|
||||||
|
const loadData = async () => {
|
||||||
|
if (!stock?.stock_code) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug('TimelineChartModal', 'loadData', '开始加载分时图数据', {
|
||||||
|
stockCode: stock.stock_code,
|
||||||
|
eventTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await stockService.getKlineData(
|
||||||
|
stock.stock_code,
|
||||||
|
'minute',
|
||||||
|
eventTime || undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response || !response.data || response.data.length === 0) {
|
||||||
|
throw new Error('暂无分时数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(response.data);
|
||||||
|
logger.info('TimelineChartModal', 'loadData', '分时图数据加载成功', {
|
||||||
|
dataCount: response.data.length,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : '数据加载失败';
|
||||||
|
logger.error('TimelineChartModal', 'loadData', err as Error);
|
||||||
|
setError(errorMsg);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartRef.current || !isOpen) return;
|
||||||
|
|
||||||
|
// 创建图表实例
|
||||||
|
chartInstance.current = echarts.init(chartRef.current, 'dark');
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
|
const handleResize = () => {
|
||||||
|
chartInstance.current?.resize();
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
chartInstance.current?.dispose();
|
||||||
|
chartInstance.current = null;
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// 更新图表数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartInstance.current || data.length === 0) return;
|
||||||
|
|
||||||
|
const times = data.map((d) => d.time);
|
||||||
|
const prices = data.map((d) => d.price);
|
||||||
|
const avgPrices = data.map((d) => d.avg_price);
|
||||||
|
const volumes = data.map((d) => d.volume);
|
||||||
|
|
||||||
|
// 计算涨跌颜色
|
||||||
|
const basePrice = data[0]?.price || 0;
|
||||||
|
const volumeColors = data.map((d) =>
|
||||||
|
d.price >= basePrice ? '#ef5350' : '#26a69a'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 图表配置
|
||||||
|
const option: echarts.EChartsOption = {
|
||||||
|
backgroundColor: '#1a1a1a',
|
||||||
|
title: {
|
||||||
|
text: `${stock?.stock_name || stock?.stock_code} - 分时图`,
|
||||||
|
left: 'center',
|
||||||
|
top: 10,
|
||||||
|
textStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: 'rgba(30, 30, 30, 0.95)',
|
||||||
|
borderColor: '#404040',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
crossStyle: {
|
||||||
|
color: '#999',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
formatter: (params: any) => {
|
||||||
|
const dataIndex = params[0]?.dataIndex;
|
||||||
|
if (dataIndex === undefined) return '';
|
||||||
|
|
||||||
|
const item = data[dataIndex];
|
||||||
|
const changeColor = item.change_percent >= 0 ? '#ef5350' : '#26a69a';
|
||||||
|
const changeSign = item.change_percent >= 0 ? '+' : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="padding: 8px;">
|
||||||
|
<div style="font-weight: bold; margin-bottom: 8px;">${item.time}</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||||
|
<span>价格:</span>
|
||||||
|
<span style="color: ${changeColor}; font-weight: bold; margin-left: 20px;">${item.price.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||||
|
<span>均价:</span>
|
||||||
|
<span style="color: #ffa726; margin-left: 20px;">${item.avg_price.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
||||||
|
<span>涨跌幅:</span>
|
||||||
|
<span style="color: ${changeColor}; margin-left: 20px;">${changeSign}${item.change_percent.toFixed(2)}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
<span>成交量:</span>
|
||||||
|
<span style="margin-left: 20px;">${(item.volume / 100).toFixed(0)}手</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: [
|
||||||
|
{
|
||||||
|
left: '5%',
|
||||||
|
right: '5%',
|
||||||
|
top: '15%',
|
||||||
|
height: '55%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
left: '5%',
|
||||||
|
right: '5%',
|
||||||
|
top: '75%',
|
||||||
|
height: '15%',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
data: times,
|
||||||
|
gridIndex: 0,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#404040',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#999',
|
||||||
|
interval: Math.floor(times.length / 6),
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#2a2a2a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
data: times,
|
||||||
|
gridIndex: 1,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#404040',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#999',
|
||||||
|
interval: Math.floor(times.length / 6),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
scale: true,
|
||||||
|
gridIndex: 0,
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#2a2a2a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#404040',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#999',
|
||||||
|
formatter: (value: number) => value.toFixed(2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: true,
|
||||||
|
gridIndex: 1,
|
||||||
|
splitLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#404040',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#999',
|
||||||
|
formatter: (value: number) => {
|
||||||
|
if (value >= 10000) {
|
||||||
|
return (value / 10000).toFixed(1) + '万';
|
||||||
|
}
|
||||||
|
return value.toFixed(0);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '价格',
|
||||||
|
type: 'line',
|
||||||
|
data: prices,
|
||||||
|
xAxisIndex: 0,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {
|
||||||
|
color: '#2196f3',
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: 'rgba(33, 150, 243, 0.3)' },
|
||||||
|
{ offset: 1, color: 'rgba(33, 150, 243, 0.05)' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '均价',
|
||||||
|
type: 'line',
|
||||||
|
data: avgPrices,
|
||||||
|
xAxisIndex: 0,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {
|
||||||
|
color: '#ffa726',
|
||||||
|
width: 1.5,
|
||||||
|
type: 'dashed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '成交量',
|
||||||
|
type: 'bar',
|
||||||
|
data: volumes,
|
||||||
|
xAxisIndex: 1,
|
||||||
|
yAxisIndex: 1,
|
||||||
|
itemStyle: {
|
||||||
|
color: (params: any) => {
|
||||||
|
return volumeColors[params.dataIndex];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'inside',
|
||||||
|
xAxisIndex: [0, 1],
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
chartInstance.current.setOption(option);
|
||||||
|
}, [data, stock]);
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
}, [isOpen, stock?.stock_code, eventTime]);
|
||||||
|
|
||||||
|
if (!stock) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size={size}>
|
||||||
|
<ModalOverlay bg="blackAlpha.700" />
|
||||||
|
<ModalContent
|
||||||
|
maxW="90vw"
|
||||||
|
maxH="85vh"
|
||||||
|
bg="#1a1a1a"
|
||||||
|
borderColor="#404040"
|
||||||
|
borderWidth="1px"
|
||||||
|
>
|
||||||
|
<ModalHeader pb={3} borderBottomWidth="1px" borderColor="#404040">
|
||||||
|
<VStack align="flex-start" spacing={1}>
|
||||||
|
<HStack>
|
||||||
|
<Text fontSize="lg" fontWeight="bold" color="#e0e0e0">
|
||||||
|
{stock.stock_name || stock.stock_code} ({stock.stock_code})
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text fontSize="sm" color="#999">
|
||||||
|
分时走势图
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton color="#999" _hover={{ color: '#e0e0e0' }} />
|
||||||
|
<ModalBody p={4}>
|
||||||
|
{error && (
|
||||||
|
<Alert status="error" bg="#2a1a1a" borderColor="#ef5350" mb={4}>
|
||||||
|
<AlertIcon color="#ef5350" />
|
||||||
|
<Text color="#e0e0e0">{error}</Text>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box position="relative" h="600px" w="100%">
|
||||||
|
{loading && (
|
||||||
|
<Flex
|
||||||
|
position="absolute"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
right="0"
|
||||||
|
bottom="0"
|
||||||
|
bg="rgba(26, 26, 26, 0.7)"
|
||||||
|
zIndex="10"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<VStack spacing={4}>
|
||||||
|
<CircularProgress isIndeterminate color="blue.400" />
|
||||||
|
<Text color="#e0e0e0">加载分时数据...</Text>
|
||||||
|
</VStack>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
<div ref={chartRef} style={{ width: '100%', height: '100%' }} />
|
||||||
|
</Box>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimelineChartModal;
|
||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
import { StarIcon } from '@chakra-ui/icons';
|
import { StarIcon } from '@chakra-ui/icons';
|
||||||
import MiniTimelineChart from '../StockDetailPanel/components/MiniTimelineChart';
|
import MiniTimelineChart from '../StockDetailPanel/components/MiniTimelineChart';
|
||||||
import MiniKLineChart from './MiniKLineChart';
|
import MiniKLineChart from './MiniKLineChart';
|
||||||
import StockChartModal from '../../../../components/StockChart/StockChartModal';
|
import TimelineChartModal from '../../../../components/StockChart/TimelineChartModal';
|
||||||
|
import KLineChartModal from '../../../../components/StockChart/KLineChartModal';
|
||||||
import CitedContent from '../../../../components/Citation/CitedContent';
|
import CitedContent from '../../../../components/Citation/CitedContent';
|
||||||
import { getChangeColor } from '../../../../utils/colorUtils';
|
import { getChangeColor } from '../../../../utils/colorUtils';
|
||||||
import { PROFESSIONAL_COLORS } from '../../../../constants/professionalTheme';
|
import { PROFESSIONAL_COLORS } from '../../../../constants/professionalTheme';
|
||||||
@@ -51,8 +52,8 @@ const StockListItem = ({
|
|||||||
const dividerColor = PROFESSIONAL_COLORS.border.default;
|
const dividerColor = PROFESSIONAL_COLORS.border.default;
|
||||||
|
|
||||||
const [isDescExpanded, setIsDescExpanded] = useState(false);
|
const [isDescExpanded, setIsDescExpanded] = useState(false);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isTimelineModalOpen, setIsTimelineModalOpen] = useState(false);
|
||||||
const [modalChartType, setModalChartType] = useState('timeline'); // 跟踪用户点击的图表类型
|
const [isKLineModalOpen, setIsKLineModalOpen] = useState(false);
|
||||||
|
|
||||||
const handleViewDetail = () => {
|
const handleViewDetail = () => {
|
||||||
const stockCode = stock.stock_code.split('.')[0];
|
const stockCode = stock.stock_code.split('.')[0];
|
||||||
@@ -204,8 +205,7 @@ const StockListItem = ({
|
|||||||
bg="rgba(59, 130, 246, 0.1)"
|
bg="rgba(59, 130, 246, 0.1)"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setModalChartType('timeline'); // 设置为分时图
|
setIsTimelineModalOpen(true);
|
||||||
setIsModalOpen(true);
|
|
||||||
}}
|
}}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
@@ -247,8 +247,7 @@ const StockListItem = ({
|
|||||||
bg="rgba(168, 85, 247, 0.1)"
|
bg="rgba(168, 85, 247, 0.1)"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setModalChartType('daily'); // 设置为日K线
|
setIsKLineModalOpen(true);
|
||||||
setIsModalOpen(true);
|
|
||||||
}}
|
}}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
@@ -380,15 +379,23 @@ const StockListItem = ({
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 股票详情弹窗 - 未打开时不渲染 */}
|
{/* 分时图弹窗 */}
|
||||||
{isModalOpen && (
|
{isTimelineModalOpen && (
|
||||||
<StockChartModal
|
<TimelineChartModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isTimelineModalOpen}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsTimelineModalOpen(false)}
|
||||||
|
stock={stock}
|
||||||
|
eventTime={eventTime}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* K线图弹窗 */}
|
||||||
|
{isKLineModalOpen && (
|
||||||
|
<KLineChartModal
|
||||||
|
isOpen={isKLineModalOpen}
|
||||||
|
onClose={() => setIsKLineModalOpen(false)}
|
||||||
stock={stock}
|
stock={stock}
|
||||||
eventTime={eventTime}
|
eventTime={eventTime}
|
||||||
size="6xl"
|
|
||||||
initialChartType={modalChartType} // 传递用户点击的图表类型
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user