Merge branch 'feature_bugfix/251201_vf_h5_ui' into feature_bugfix/251201_py_h5_ui
* feature_bugfix/251201_vf_h5_ui: feat: 日k 和 分时h5UI调整 fix: 弹窗固定高度 feat: K线添加mock数据 feat: 添加批量获取K线数据的 mock handler
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
// src/components/StockChart/KLineChartModal.tsx - K线图弹窗组件
|
// src/components/StockChart/KLineChartModal.tsx - K线图弹窗组件
|
||||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { stockService } from '@services/eventService';
|
import { stockService } from '@services/eventService';
|
||||||
|
import { selectIsMobile } from '@store/slices/deviceSlice';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 股票信息
|
* 股票信息
|
||||||
@@ -83,6 +85,9 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
const [earliestDate, setEarliestDate] = useState<string | null>(null);
|
const [earliestDate, setEarliestDate] = useState<string | null>(null);
|
||||||
const [totalDaysLoaded, setTotalDaysLoaded] = useState(0);
|
const [totalDaysLoaded, setTotalDaysLoaded] = useState(0);
|
||||||
|
|
||||||
|
// H5 响应式适配
|
||||||
|
const isMobile = useSelector(selectIsMobile);
|
||||||
|
|
||||||
// 调试日志
|
// 调试日志
|
||||||
console.log('[KLineChartModal] 渲染状态:', {
|
console.log('[KLineChartModal] 渲染状态:', {
|
||||||
isOpen,
|
isOpen,
|
||||||
@@ -296,16 +301,16 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 图表配置
|
// 图表配置(H5 响应式)
|
||||||
const option: echarts.EChartsOption = {
|
const option: echarts.EChartsOption = {
|
||||||
backgroundColor: '#1a1a1a',
|
backgroundColor: '#1a1a1a',
|
||||||
title: {
|
title: {
|
||||||
text: `${stock?.stock_name || stock?.stock_code} - 日K线`,
|
text: `${stock?.stock_name || stock?.stock_code} - 日K线`,
|
||||||
left: 'center',
|
left: 'center',
|
||||||
top: 10,
|
top: isMobile ? 5 : 10,
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#e0e0e0',
|
color: '#e0e0e0',
|
||||||
fontSize: 18,
|
fontSize: isMobile ? 14 : 18,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -370,16 +375,16 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
},
|
},
|
||||||
grid: [
|
grid: [
|
||||||
{
|
{
|
||||||
left: '5%',
|
left: isMobile ? '12%' : '5%',
|
||||||
right: '5%',
|
right: isMobile ? '5%' : '5%',
|
||||||
top: '12%',
|
top: isMobile ? '12%' : '12%',
|
||||||
height: '60%',
|
height: isMobile ? '55%' : '60%',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
left: '5%',
|
left: isMobile ? '12%' : '5%',
|
||||||
right: '5%',
|
right: isMobile ? '5%' : '5%',
|
||||||
top: '77%',
|
top: isMobile ? '72%' : '77%',
|
||||||
height: '18%',
|
height: isMobile ? '20%' : '18%',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
xAxis: [
|
xAxis: [
|
||||||
@@ -394,7 +399,8 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#999',
|
color: '#999',
|
||||||
interval: Math.floor(dates.length / 8),
|
fontSize: isMobile ? 10 : 12,
|
||||||
|
interval: Math.floor(dates.length / (isMobile ? 4 : 8)),
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: false,
|
show: false,
|
||||||
@@ -411,7 +417,8 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#999',
|
color: '#999',
|
||||||
interval: Math.floor(dates.length / 8),
|
fontSize: isMobile ? 10 : 12,
|
||||||
|
interval: Math.floor(dates.length / (isMobile ? 4 : 8)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -419,6 +426,7 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
{
|
{
|
||||||
scale: true,
|
scale: true,
|
||||||
gridIndex: 0,
|
gridIndex: 0,
|
||||||
|
splitNumber: isMobile ? 4 : 5,
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: true,
|
show: true,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
@@ -432,12 +440,14 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#999',
|
color: '#999',
|
||||||
|
fontSize: isMobile ? 10 : 12,
|
||||||
formatter: (value: number) => value.toFixed(2),
|
formatter: (value: number) => value.toFixed(2),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scale: true,
|
scale: true,
|
||||||
gridIndex: 1,
|
gridIndex: 1,
|
||||||
|
splitNumber: isMobile ? 2 : 3,
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
@@ -448,6 +458,7 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#999',
|
color: '#999',
|
||||||
|
fontSize: isMobile ? 10 : 12,
|
||||||
formatter: (value: number) => {
|
formatter: (value: number) => {
|
||||||
if (value >= 100000000) {
|
if (value >= 100000000) {
|
||||||
return (value / 100000000).toFixed(1) + '亿';
|
return (value / 100000000).toFixed(1) + '亿';
|
||||||
@@ -545,7 +556,7 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
|
|
||||||
return () => clearTimeout(retryTimer);
|
return () => clearTimeout(retryTimer);
|
||||||
}
|
}
|
||||||
}, [data, stock]);
|
}, [data, stock, isMobile]);
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -600,13 +611,13 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
top: '50%',
|
top: '50%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
width: '90vw',
|
width: isMobile ? '96vw' : '90vw',
|
||||||
maxWidth: '1400px',
|
maxWidth: isMobile ? 'none' : '1400px',
|
||||||
maxHeight: '85vh',
|
maxHeight: isMobile ? '85vh' : '85vh',
|
||||||
backgroundColor: '#1a1a1a',
|
backgroundColor: '#1a1a1a',
|
||||||
border: '2px solid #ffd700',
|
border: '2px solid #ffd700',
|
||||||
boxShadow: '0 0 30px rgba(255, 215, 0, 0.5)',
|
boxShadow: '0 0 30px rgba(255, 215, 0, 0.5)',
|
||||||
borderRadius: '8px',
|
borderRadius: isMobile ? '12px' : '8px',
|
||||||
zIndex: 10002,
|
zIndex: 10002,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -616,7 +627,7 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: '16px 24px',
|
padding: isMobile ? '12px 16px' : '16px 24px',
|
||||||
borderBottom: '1px solid #404040',
|
borderBottom: '1px solid #404040',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
@@ -624,18 +635,18 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? '8px' : '12px', flexWrap: isMobile ? 'wrap' : 'nowrap' }}>
|
||||||
<span style={{ fontSize: '18px', fontWeight: 'bold', color: '#e0e0e0' }}>
|
<span style={{ fontSize: isMobile ? '14px' : '18px', fontWeight: 'bold', color: '#e0e0e0' }}>
|
||||||
{stock.stock_name || stock.stock_code} ({stock.stock_code})
|
{stock.stock_name || stock.stock_code} ({stock.stock_code})
|
||||||
</span>
|
</span>
|
||||||
{data.length > 0 && (
|
{data.length > 0 && (
|
||||||
<span style={{ fontSize: '12px', color: '#666', fontStyle: 'italic' }}>
|
<span style={{ fontSize: isMobile ? '10px' : '12px', color: '#666', fontStyle: 'italic' }}>
|
||||||
共{data.length}个交易日
|
共{data.length}个交易日
|
||||||
{hasMore ? '(向左滑动加载更多)' : '(已加载全部)'}
|
{hasMore ? '(向左滑动加载更多)' : '(已加载全部)'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{loadingMore && (
|
{loadingMore && (
|
||||||
<span style={{ fontSize: '12px', color: '#3182ce', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<span style={{ fontSize: isMobile ? '10px' : '12px', color: '#3182ce', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
<span style={{
|
<span style={{
|
||||||
width: '12px',
|
width: '12px',
|
||||||
height: '12px',
|
height: '12px',
|
||||||
@@ -649,10 +660,10 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', marginTop: '4px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? '8px' : '16px', marginTop: '4px' }}>
|
||||||
<span style={{ fontSize: '14px', color: '#999' }}>日K线图</span>
|
<span style={{ fontSize: isMobile ? '12px' : '14px', color: '#999' }}>日K线图</span>
|
||||||
<span style={{ fontSize: '12px', color: '#666' }}>
|
<span style={{ fontSize: isMobile ? '10px' : '12px', color: '#666' }}>
|
||||||
💡 鼠标滚轮缩放 | 拖动查看不同时间段
|
💡 {isMobile ? '滚轮缩放 | 拖动查看' : '鼠标滚轮缩放 | 拖动查看不同时间段'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -675,26 +686,33 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div style={{ padding: '16px', flex: 1, overflow: 'auto' }}>
|
<div style={{
|
||||||
|
padding: isMobile ? '8px' : '16px',
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
{error && (
|
{error && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#2a1a1a',
|
backgroundColor: '#2a1a1a',
|
||||||
border: '1px solid #ef5350',
|
border: '1px solid #ef5350',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
padding: '12px 16px',
|
padding: isMobile ? '8px 12px' : '12px 16px',
|
||||||
marginBottom: '16px',
|
marginBottom: isMobile ? '8px' : '16px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ color: '#ef5350' }}>⚠</span>
|
<span style={{ color: '#ef5350' }}>⚠</span>
|
||||||
<span style={{ color: '#e0e0e0' }}>{error}</span>
|
<span style={{ color: '#e0e0e0', fontSize: isMobile ? '12px' : '14px' }}>{error}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ position: 'relative', height: '680px', width: '100%' }}>
|
<div style={{ position: 'relative', height: isMobile ? '450px' : '680px', width: '100%' }}>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// src/components/StockChart/TimelineChartModal.tsx - 分时图弹窗组件
|
// src/components/StockChart/TimelineChartModal.tsx - 分时图弹窗组件
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { klineDataCache, getCacheKey, fetchKlineData } from '@views/Community/components/StockDetailPanel/utils/klineDataCache';
|
import { klineDataCache, getCacheKey, fetchKlineData } from '@views/Community/components/StockDetailPanel/utils/klineDataCache';
|
||||||
|
import { selectIsMobile } from '@store/slices/deviceSlice';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 股票信息
|
* 股票信息
|
||||||
@@ -68,6 +70,9 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [data, setData] = useState<TimelineDataPoint[]>([]);
|
const [data, setData] = useState<TimelineDataPoint[]>([]);
|
||||||
|
|
||||||
|
// H5 响应式适配
|
||||||
|
const isMobile = useSelector(selectIsMobile);
|
||||||
|
|
||||||
// 加载分时图数据(优先使用缓存)
|
// 加载分时图数据(优先使用缓存)
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
if (!stock?.stock_code) return;
|
if (!stock?.stock_code) return;
|
||||||
@@ -187,16 +192,16 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 图表配置
|
// 图表配置(H5 响应式)
|
||||||
const option: echarts.EChartsOption = {
|
const option: echarts.EChartsOption = {
|
||||||
backgroundColor: '#1a1a1a',
|
backgroundColor: '#1a1a1a',
|
||||||
title: {
|
title: {
|
||||||
text: `${stock?.stock_name || stock?.stock_code} - 分时图`,
|
text: `${stock?.stock_name || stock?.stock_code} - 分时图`,
|
||||||
left: 'center',
|
left: 'center',
|
||||||
top: 10,
|
top: isMobile ? 5 : 10,
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#e0e0e0',
|
color: '#e0e0e0',
|
||||||
fontSize: 18,
|
fontSize: isMobile ? 14 : 18,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -247,16 +252,16 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
},
|
},
|
||||||
grid: [
|
grid: [
|
||||||
{
|
{
|
||||||
left: '5%',
|
left: isMobile ? '12%' : '5%',
|
||||||
right: '5%',
|
right: isMobile ? '5%' : '5%',
|
||||||
top: '15%',
|
top: isMobile ? '12%' : '15%',
|
||||||
height: '55%',
|
height: isMobile ? '58%' : '55%',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
left: '5%',
|
left: isMobile ? '12%' : '5%',
|
||||||
right: '5%',
|
right: isMobile ? '5%' : '5%',
|
||||||
top: '75%',
|
top: isMobile ? '75%' : '75%',
|
||||||
height: '15%',
|
height: isMobile ? '18%' : '15%',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
xAxis: [
|
xAxis: [
|
||||||
@@ -271,7 +276,8 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#999',
|
color: '#999',
|
||||||
interval: Math.floor(times.length / 6),
|
fontSize: isMobile ? 10 : 12,
|
||||||
|
interval: Math.floor(times.length / (isMobile ? 4 : 6)),
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: true,
|
show: true,
|
||||||
@@ -291,7 +297,8 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#999',
|
color: '#999',
|
||||||
interval: Math.floor(times.length / 6),
|
fontSize: isMobile ? 10 : 12,
|
||||||
|
interval: Math.floor(times.length / (isMobile ? 4 : 6)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -299,6 +306,7 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
{
|
{
|
||||||
scale: true,
|
scale: true,
|
||||||
gridIndex: 0,
|
gridIndex: 0,
|
||||||
|
splitNumber: isMobile ? 4 : 5,
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: true,
|
show: true,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
@@ -312,12 +320,14 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#999',
|
color: '#999',
|
||||||
|
fontSize: isMobile ? 10 : 12,
|
||||||
formatter: (value: number) => value.toFixed(2),
|
formatter: (value: number) => value.toFixed(2),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scale: true,
|
scale: true,
|
||||||
gridIndex: 1,
|
gridIndex: 1,
|
||||||
|
splitNumber: isMobile ? 2 : 3,
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
@@ -328,6 +338,7 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#999',
|
color: '#999',
|
||||||
|
fontSize: isMobile ? 10 : 12,
|
||||||
formatter: (value: number) => {
|
formatter: (value: number) => {
|
||||||
if (value >= 10000) {
|
if (value >= 10000) {
|
||||||
return (value / 10000).toFixed(1) + '万';
|
return (value / 10000).toFixed(1) + '万';
|
||||||
@@ -443,7 +454,7 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
|
|
||||||
return () => clearTimeout(retryTimer);
|
return () => clearTimeout(retryTimer);
|
||||||
}
|
}
|
||||||
}, [data, stock]);
|
}, [data, stock, isMobile]);
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -455,29 +466,30 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
if (!stock) return null;
|
if (!stock) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size={size}>
|
<Modal isOpen={isOpen} onClose={onClose} size={size} isCentered>
|
||||||
<ModalOverlay bg="blackAlpha.700" />
|
<ModalOverlay bg="blackAlpha.700" />
|
||||||
<ModalContent
|
<ModalContent
|
||||||
maxW="90vw"
|
maxW={isMobile ? '96vw' : '90vw'}
|
||||||
maxH="85vh"
|
maxH="85vh"
|
||||||
|
borderRadius={isMobile ? '12px' : '8px'}
|
||||||
bg="#1a1a1a"
|
bg="#1a1a1a"
|
||||||
borderColor="#404040"
|
border="2px solid #ffd700"
|
||||||
borderWidth="1px"
|
boxShadow="0 0 30px rgba(255, 215, 0, 0.5)"
|
||||||
>
|
>
|
||||||
<ModalHeader pb={3} borderBottomWidth="1px" borderColor="#404040">
|
<ModalHeader pb={isMobile ? 2 : 3} borderBottomWidth="1px" borderColor="#404040">
|
||||||
<VStack align="flex-start" spacing={1}>
|
<VStack align="flex-start" spacing={0}>
|
||||||
<HStack>
|
<HStack>
|
||||||
<Text fontSize="lg" fontWeight="bold" color="#e0e0e0">
|
<Text fontSize={isMobile ? 'md' : 'lg'} fontWeight="bold" color="#e0e0e0">
|
||||||
{stock.stock_name || stock.stock_code} ({stock.stock_code})
|
{stock.stock_name || stock.stock_code} ({stock.stock_code})
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text fontSize="sm" color="#999">
|
<Text fontSize={isMobile ? 'xs' : 'sm'} color="#999">
|
||||||
分时走势图
|
分时走势图
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalCloseButton color="#999" _hover={{ color: '#e0e0e0' }} />
|
<ModalCloseButton color="#999" _hover={{ color: '#e0e0e0' }} />
|
||||||
<ModalBody p={4}>
|
<ModalBody p={isMobile ? 2 : 4}>
|
||||||
{error && (
|
{error && (
|
||||||
<Alert status="error" bg="#2a1a1a" borderColor="#ef5350" mb={4}>
|
<Alert status="error" bg="#2a1a1a" borderColor="#ef5350" mb={4}>
|
||||||
<AlertIcon color="#ef5350" />
|
<AlertIcon color="#ef5350" />
|
||||||
@@ -485,7 +497,7 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box position="relative" h="600px" w="100%">
|
<Box position="relative" h={isMobile ? '400px' : '600px'} w="100%">
|
||||||
{loading && (
|
{loading && (
|
||||||
<Flex
|
<Flex
|
||||||
position="absolute"
|
position="absolute"
|
||||||
|
|||||||
@@ -61,6 +61,20 @@ export const generateDailyData = (indexCode, days = 30) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算简单移动均价(用于分时图均价线)
|
||||||
|
* @param {Array} data - 已有数据
|
||||||
|
* @param {number} currentPrice - 当前价格
|
||||||
|
* @param {number} period - 均线周期(默认5)
|
||||||
|
* @returns {number} 均价
|
||||||
|
*/
|
||||||
|
function calculateAvgPrice(data, currentPrice, period = 5) {
|
||||||
|
const recentPrices = data.slice(-period).map(d => d.price || d.close);
|
||||||
|
recentPrices.push(currentPrice);
|
||||||
|
const sum = recentPrices.reduce((acc, p) => acc + p, 0);
|
||||||
|
return parseFloat((sum / recentPrices.length).toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成时间范围内的数据
|
* 生成时间范围内的数据
|
||||||
*/
|
*/
|
||||||
@@ -80,6 +94,11 @@ function generateTimeRange(data, startTime, endTime, basePrice, session) {
|
|||||||
|
|
||||||
// ✅ 修复:为分时图添加完整的 OHLC 字段
|
// ✅ 修复:为分时图添加完整的 OHLC 字段
|
||||||
const closePrice = parseFloat(price.toFixed(2));
|
const closePrice = parseFloat(price.toFixed(2));
|
||||||
|
|
||||||
|
// 计算均价和涨跌幅
|
||||||
|
const avgPrice = calculateAvgPrice(data, closePrice);
|
||||||
|
const changePercent = parseFloat(((closePrice - basePrice) / basePrice * 100).toFixed(2));
|
||||||
|
|
||||||
data.push({
|
data.push({
|
||||||
time: formatTime(current),
|
time: formatTime(current),
|
||||||
timestamp: current.getTime(), // ✅ 新增:毫秒时间戳
|
timestamp: current.getTime(), // ✅ 新增:毫秒时间戳
|
||||||
@@ -88,6 +107,8 @@ function generateTimeRange(data, startTime, endTime, basePrice, session) {
|
|||||||
low: parseFloat((price * 0.9997).toFixed(2)), // ✅ 新增:最低价(略低于收盘)
|
low: parseFloat((price * 0.9997).toFixed(2)), // ✅ 新增:最低价(略低于收盘)
|
||||||
close: closePrice, // ✅ 保留:收盘价
|
close: closePrice, // ✅ 保留:收盘价
|
||||||
price: closePrice, // ✅ 保留:兼容字段(供 MiniTimelineChart 使用)
|
price: closePrice, // ✅ 保留:兼容字段(供 MiniTimelineChart 使用)
|
||||||
|
avg_price: avgPrice, // ✅ 新增:均价(供 TimelineChartModal 使用)
|
||||||
|
change_percent: changePercent, // ✅ 新增:涨跌幅(供 TimelineChartModal 使用)
|
||||||
volume: volume,
|
volume: volume,
|
||||||
prev_close: basePrice
|
prev_close: basePrice
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -224,4 +224,59 @@ export const stockHandlers = [
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// 批量获取股票K线数据
|
||||||
|
http.post('/api/stock/batch-kline', async ({ request }) => {
|
||||||
|
await delay(400);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { codes, type = 'timeline', event_time } = body;
|
||||||
|
|
||||||
|
console.log('[Mock Stock] 批量获取K线数据:', {
|
||||||
|
stockCount: codes?.length,
|
||||||
|
type,
|
||||||
|
eventTime: event_time
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!codes || !Array.isArray(codes) || codes.length === 0) {
|
||||||
|
return HttpResponse.json(
|
||||||
|
{ error: '股票代码列表不能为空' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每只股票生成数据
|
||||||
|
const batchData = {};
|
||||||
|
codes.forEach(stockCode => {
|
||||||
|
let data;
|
||||||
|
if (type === 'timeline') {
|
||||||
|
data = generateTimelineData('000001.SH');
|
||||||
|
} else if (type === 'daily') {
|
||||||
|
data = generateDailyData('000001.SH', 60);
|
||||||
|
} else {
|
||||||
|
data = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
batchData[stockCode] = {
|
||||||
|
success: true,
|
||||||
|
data: data,
|
||||||
|
stock_code: stockCode
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return HttpResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: batchData,
|
||||||
|
type: type,
|
||||||
|
message: '批量获取成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Mock Stock] 批量获取K线数据失败:', error);
|
||||||
|
return HttpResponse.json(
|
||||||
|
{ error: '批量获取K线数据失败' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ const EventDetailModal: React.FC<EventDetailModalProps> = ({
|
|||||||
className="event-detail-modal"
|
className="event-detail-modal"
|
||||||
styles={{
|
styles={{
|
||||||
mask: { background: 'transparent' },
|
mask: { background: 'transparent' },
|
||||||
content: { borderRadius: 24, padding: 0, maxWidth: 1400, background: 'transparent', margin: '0 auto' },
|
content: { borderRadius: 24, padding: 0, maxWidth: 1400, background: 'transparent', margin: '0 auto', maxHeight: '80vh', display: 'flex', flexDirection: 'column' },
|
||||||
header: { background: '#FFFFFF', borderBottom: '1px solid #E2E8F0', padding: '16px 24px', borderRadius: '24px 24px 0 0', margin: 0 },
|
header: { background: '#FFFFFF', borderBottom: '1px solid #E2E8F0', padding: '16px 24px', borderRadius: '24px 24px 0 0', margin: 0, flexShrink: 0 },
|
||||||
body: { padding: 0 },
|
body: { padding: 0, overflowY: 'auto', flex: 1 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{event && <DynamicNewsDetailPanel event={event} showHeader={false} />}
|
{event && <DynamicNewsDetailPanel event={event} showHeader={false} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user