update pay promo
This commit is contained in:
@@ -628,7 +628,11 @@ export const stockHandlers = [
|
||||
total_shares: `${(Math.random() * 100 + 10).toFixed(2)}亿`,
|
||||
circulating_shares: `${(Math.random() * 80 + 5).toFixed(2)}亿`,
|
||||
week52_high: parseFloat((basePrice * 1.3).toFixed(2)),
|
||||
week52_low: parseFloat((basePrice * 0.7).toFixed(2))
|
||||
week52_low: parseFloat((basePrice * 0.7).toFixed(2)),
|
||||
// 主力动态(当日快照)
|
||||
net_inflow: parseFloat((Math.random() * 20000 - 10000).toFixed(2)),
|
||||
main_inflow_ratio: parseFloat((Math.random() * 20 - 10).toFixed(2)),
|
||||
net_active_buy_ratio: parseFloat((Math.random() * 60 - 30).toFixed(2)),
|
||||
},
|
||||
message: '获取成功'
|
||||
});
|
||||
@@ -687,4 +691,68 @@ export const stockHandlers = [
|
||||
message: '获取成功'
|
||||
});
|
||||
}),
|
||||
|
||||
// 主力资金流时间序列数据
|
||||
http.get('/api/stock/:stockCode/main-capital-flow', async ({ params, request }) => {
|
||||
await delay(150);
|
||||
|
||||
const { stockCode } = params;
|
||||
const url = new URL(request.url);
|
||||
const days = parseInt(url.searchParams.get('days') || '20', 10);
|
||||
|
||||
console.log('[Mock Stock] 获取主力资金流时间序列:', { stockCode, days });
|
||||
|
||||
// 生成指定天数的模拟数据
|
||||
const items = [];
|
||||
const today = new Date();
|
||||
|
||||
// 使用股票代码作为种子让同一只股票的数据相对稳定
|
||||
const codeSeed = parseInt(stockCode.replace(/\D/g, '').slice(0, 6), 10) || 12345;
|
||||
|
||||
// 模拟一个趋势(先生成一个基准线,然后在上面加噪声)
|
||||
let trend = (codeSeed % 5 - 2) * 500; // -1000 ~ 1000 的趋势基准
|
||||
let cumulativeInflow = 0;
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date(today);
|
||||
date.setDate(date.getDate() - i);
|
||||
|
||||
// 跳过周末
|
||||
const dayOfWeek = date.getDay();
|
||||
if (dayOfWeek === 0 || dayOfWeek === 6) continue;
|
||||
|
||||
// 带趋势的随机净流入(万元)
|
||||
const noise = (Math.random() - 0.5) * 8000;
|
||||
const trendShift = trend * (1 + (Math.random() - 0.5) * 0.3);
|
||||
const netInflow = parseFloat((trendShift + noise).toFixed(2));
|
||||
cumulativeInflow += netInflow;
|
||||
|
||||
// 占比 = 净流入 / 日均成交额 * 100(模拟)
|
||||
const mainInflowRatio = parseFloat((netInflow / (Math.random() * 30000 + 20000) * 100).toFixed(2));
|
||||
|
||||
// 净主动买入占比:与净流入正相关但有随机偏移
|
||||
const netActiveBuyRatio = parseFloat(
|
||||
(Math.sign(netInflow) * Math.random() * 25 + (netInflow > 0 ? 5 : -5) + (Math.random() - 0.5) * 15).toFixed(2)
|
||||
);
|
||||
|
||||
items.push({
|
||||
trade_date: date.toISOString().split('T')[0],
|
||||
net_inflow: netInflow,
|
||||
main_inflow_ratio: mainInflowRatio,
|
||||
net_active_buy_ratio: Math.max(-50, Math.min(50, netActiveBuyRatio)),
|
||||
});
|
||||
|
||||
// 趋势缓慢漂移
|
||||
trend += (Math.random() - 0.5) * 200;
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
code: stockCode,
|
||||
items,
|
||||
},
|
||||
message: '获取成功'
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
/**
|
||||
* MainCapitalFlowSection - 主力资金流时间序列组件
|
||||
*
|
||||
* 替代原有的简单快照展示,提供完整的时间序列视图:
|
||||
* - 左侧:今日关键指标摘要(净流入、占比、净主动买入)
|
||||
* - 右侧:ECharts 柱状图(净流入红绿柱)+ 折线图(占比趋势)
|
||||
* - 底部:时间范围选择器(5日/10日/20日)
|
||||
*
|
||||
* 数据来源:stock_main_capital_flow 表
|
||||
*/
|
||||
|
||||
import React, { memo, useMemo, useState, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Spinner,
|
||||
Progress,
|
||||
} from '@chakra-ui/react';
|
||||
import EChartsWrapper from '../../EChartsWrapper';
|
||||
import { DEEP_SPACE_THEME as T } from './theme';
|
||||
import { useMainCapitalFlow } from '../hooks';
|
||||
import type { MainCapitalFlowItem } from '../types';
|
||||
|
||||
// ============================================
|
||||
// 时间范围配置
|
||||
// ============================================
|
||||
const PERIOD_OPTIONS = [
|
||||
{ label: '5日', days: 10 }, // 请求10天以确保拿到5个交易日
|
||||
{ label: '10日', days: 18 },
|
||||
{ label: '20日', days: 35 },
|
||||
] as const;
|
||||
|
||||
type PeriodLabel = typeof PERIOD_OPTIONS[number]['label'];
|
||||
|
||||
// ============================================
|
||||
// 子组件:今日摘要指标
|
||||
// ============================================
|
||||
interface TodaySummaryProps {
|
||||
latestItem: MainCapitalFlowItem | null;
|
||||
}
|
||||
|
||||
/** 格式化万元数值,大值自动转亿 */
|
||||
const formatWanYuan = (value: number | null): string => {
|
||||
if (value === null || value === undefined) return '--';
|
||||
const abs = Math.abs(value);
|
||||
const sign = value >= 0 ? '+' : '';
|
||||
if (abs >= 10000) {
|
||||
return `${sign}${(value / 10000).toFixed(2)}亿`;
|
||||
}
|
||||
return `${sign}${value.toFixed(0)}万`;
|
||||
};
|
||||
|
||||
const formatPercent = (value: number | null): string => {
|
||||
if (value === null || value === undefined) return '--';
|
||||
const sign = value >= 0 ? '+' : '';
|
||||
return `${sign}${value.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
const TodaySummary: React.FC<TodaySummaryProps> = memo(({ latestItem }) => {
|
||||
const netInflow = latestItem?.netInflow ?? null;
|
||||
const mainInflowRatio = latestItem?.mainInflowRatio ?? null;
|
||||
const netActiveBuyRatio = latestItem?.netActiveBuyRatio ?? null;
|
||||
|
||||
const inflowColor = (netInflow ?? 0) >= 0 ? T.upColor : T.downColor;
|
||||
const ratioColor = (mainInflowRatio ?? 0) >= 0 ? T.upColor : T.downColor;
|
||||
const buyRatioValue = netActiveBuyRatio ?? 0;
|
||||
const progressValue = Math.min(100, Math.max(0, 50 + buyRatioValue / 2));
|
||||
|
||||
return (
|
||||
<VStack align="stretch" spacing={3} minW="160px">
|
||||
{/* 今日标签 */}
|
||||
<Text fontSize="11px" color={T.textMuted} letterSpacing="0.05em">
|
||||
{latestItem?.tradeDate || '今日'}
|
||||
</Text>
|
||||
|
||||
{/* 主力净流入 */}
|
||||
<Box>
|
||||
<Text fontSize="11px" color={T.textMuted} mb={1}>主力净流入</Text>
|
||||
<Text
|
||||
fontSize="20px"
|
||||
fontWeight="700"
|
||||
color={inflowColor}
|
||||
textShadow={`0 0 12px ${inflowColor}40`}
|
||||
fontFamily="'Menlo', 'Monaco', monospace"
|
||||
lineHeight="1.2"
|
||||
>
|
||||
{formatWanYuan(netInflow)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 流入占比 */}
|
||||
<HStack justify="space-between" fontSize="12px">
|
||||
<Text color={T.textMuted}>流入占比</Text>
|
||||
<Text color={ratioColor} fontWeight="600">
|
||||
{formatPercent(mainInflowRatio)}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 净主动买入占比 - 进度条 */}
|
||||
<Box>
|
||||
<HStack justify="space-between" mb={1} fontSize="11px">
|
||||
<Text color={T.textMuted}>净主动买入</Text>
|
||||
<Text
|
||||
color={buyRatioValue >= 0 ? T.upColor : T.downColor}
|
||||
fontWeight="600"
|
||||
fontSize="12px"
|
||||
>
|
||||
{formatPercent(netActiveBuyRatio)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Box position="relative">
|
||||
<Progress
|
||||
value={progressValue}
|
||||
size="sm"
|
||||
sx={{
|
||||
'& > div': {
|
||||
bg: buyRatioValue >= 0 ? T.upColor : T.downColor,
|
||||
boxShadow: buyRatioValue >= 0 ? T.upGlow : T.downGlow,
|
||||
transition: 'all 0.3s ease',
|
||||
},
|
||||
}}
|
||||
bg="rgba(255,255,255,0.1)"
|
||||
borderRadius="full"
|
||||
h="6px"
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
left="50%"
|
||||
top="0"
|
||||
bottom="0"
|
||||
w="1px"
|
||||
bg="rgba(255,255,255,0.3)"
|
||||
transform="translateX(-50%)"
|
||||
/>
|
||||
</Box>
|
||||
<HStack justify="space-between" mt={0.5} fontSize="10px">
|
||||
<Text color={T.downColor}>卖出</Text>
|
||||
<Text color={T.upColor}>买入</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
);
|
||||
});
|
||||
TodaySummary.displayName = 'TodaySummary';
|
||||
|
||||
// ============================================
|
||||
// ECharts 图表配置
|
||||
// ============================================
|
||||
const buildChartOption = (items: MainCapitalFlowItem[]) => {
|
||||
const dates = items.map((d) => {
|
||||
// 只显示月-日
|
||||
const parts = d.tradeDate.split('-');
|
||||
return `${parts[1]}-${parts[2]}`;
|
||||
});
|
||||
|
||||
const netInflowData = items.map((d) => d.netInflow);
|
||||
const ratioData = items.map((d) => d.mainInflowRatio);
|
||||
|
||||
// 计算累计净流入
|
||||
let cumulative = 0;
|
||||
const cumulativeData = items.map((d) => {
|
||||
cumulative += d.netInflow;
|
||||
return parseFloat(cumulative.toFixed(2));
|
||||
});
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(15, 18, 35, 0.95)',
|
||||
borderColor: 'rgba(212, 175, 55, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#F5F0E1', fontSize: 12 },
|
||||
formatter: (params: any[]) => {
|
||||
if (!params || params.length === 0) return '';
|
||||
const idx = params[0].dataIndex;
|
||||
const item = items[idx];
|
||||
if (!item) return '';
|
||||
|
||||
const inflowColor = item.netInflow >= 0 ? T.upColor : T.downColor;
|
||||
const ratioColor = item.mainInflowRatio >= 0 ? T.upColor : T.downColor;
|
||||
|
||||
return `
|
||||
<div style="font-weight:600;font-size:13px;color:${T.gold};margin-bottom:6px">
|
||||
${item.tradeDate}
|
||||
</div>
|
||||
<div style="margin:3px 0;display:flex;justify-content:space-between;gap:20px">
|
||||
<span>主力净流入</span>
|
||||
<span style="color:${inflowColor};font-weight:600;font-family:'Menlo',monospace">
|
||||
${formatWanYuan(item.netInflow)}
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin:3px 0;display:flex;justify-content:space-between;gap:20px">
|
||||
<span>流入占比</span>
|
||||
<span style="color:${ratioColor};font-weight:600;font-family:'Menlo',monospace">
|
||||
${formatPercent(item.mainInflowRatio)}
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin:3px 0;display:flex;justify-content:space-between;gap:20px">
|
||||
<span>累计净流入</span>
|
||||
<span style="color:${cumulativeData[idx] >= 0 ? T.upColor : T.downColor};font-weight:600;font-family:'Menlo',monospace">
|
||||
${formatWanYuan(cumulativeData[idx])}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ['主力净流入', '累计净流入', '流入占比'],
|
||||
top: 0,
|
||||
right: 0,
|
||||
textStyle: { color: 'rgba(235, 230, 215, 0.7)', fontSize: 11 },
|
||||
itemWidth: 12,
|
||||
itemHeight: 8,
|
||||
itemGap: 12,
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(235, 230, 215, 0.6)',
|
||||
fontSize: 10,
|
||||
rotate: items.length > 15 ? 45 : 0,
|
||||
},
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '万元',
|
||||
nameTextStyle: { color: 'rgba(235, 230, 215, 0.5)', fontSize: 10 },
|
||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.05)' } },
|
||||
axisLabel: {
|
||||
color: 'rgba(235, 230, 215, 0.5)',
|
||||
fontSize: 10,
|
||||
formatter: (v: number) => {
|
||||
const abs = Math.abs(v);
|
||||
if (abs >= 10000) return `${(v / 10000).toFixed(1)}亿`;
|
||||
if (abs >= 1000) return `${(v / 1000).toFixed(1)}千`;
|
||||
return `${v}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '%',
|
||||
nameTextStyle: { color: 'rgba(235, 230, 215, 0.5)', fontSize: 10 },
|
||||
splitLine: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(235, 230, 215, 0.5)',
|
||||
fontSize: 10,
|
||||
formatter: '{value}%',
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '主力净流入',
|
||||
type: 'bar',
|
||||
data: netInflowData.map((v) => ({
|
||||
value: v,
|
||||
itemStyle: {
|
||||
color: v >= 0 ? T.upColor : T.downColor,
|
||||
borderRadius: v >= 0 ? [2, 2, 0, 0] : [0, 0, 2, 2],
|
||||
},
|
||||
})),
|
||||
barMaxWidth: 20,
|
||||
},
|
||||
{
|
||||
name: '累计净流入',
|
||||
type: 'line',
|
||||
data: cumulativeData,
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: { width: 2, color: T.gold },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(212, 175, 55, 0.2)' },
|
||||
{ offset: 1, color: 'rgba(212, 175, 55, 0.02)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '流入占比',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: ratioData,
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: { width: 1.5, color: T.cyan, type: 'dashed' },
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 主组件
|
||||
// ============================================
|
||||
export interface MainCapitalFlowSectionProps {
|
||||
stockCode?: string;
|
||||
}
|
||||
|
||||
export const MainCapitalFlowSection: React.FC<MainCapitalFlowSectionProps> = memo(
|
||||
({ stockCode }) => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState<PeriodLabel>('10日');
|
||||
|
||||
// 根据选中周期获取请求天数
|
||||
const requestDays = useMemo(
|
||||
() => PERIOD_OPTIONS.find((p) => p.label === selectedPeriod)?.days ?? 18,
|
||||
[selectedPeriod],
|
||||
);
|
||||
|
||||
const { items, isLoading } = useMainCapitalFlow(stockCode, requestDays);
|
||||
|
||||
// 按选中周期裁剪数据(取最后 N 个交易日)
|
||||
const displayItems = useMemo(() => {
|
||||
const targetCount = parseInt(selectedPeriod, 10); // "5日" -> 5
|
||||
if (items.length <= targetCount) return items;
|
||||
return items.slice(-targetCount);
|
||||
}, [items, selectedPeriod]);
|
||||
|
||||
// 最新一天的数据
|
||||
const latestItem = useMemo(
|
||||
() => (displayItems.length > 0 ? displayItems[displayItems.length - 1] : null),
|
||||
[displayItems],
|
||||
);
|
||||
|
||||
// 图表配置
|
||||
const chartOption = useMemo(
|
||||
() => (displayItems.length > 0 ? buildChartOption(displayItems) : null),
|
||||
[displayItems],
|
||||
);
|
||||
|
||||
const handlePeriodChange = useCallback((label: PeriodLabel) => {
|
||||
setSelectedPeriod(label);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg={T.bgInset}
|
||||
borderRadius={T.radiusLG}
|
||||
border={`1px solid ${T.borderGlass}`}
|
||||
p={4}
|
||||
position="relative"
|
||||
transition={T.transitionFast}
|
||||
_hover={{
|
||||
borderColor: T.borderGoldHover,
|
||||
bg: 'rgba(15, 18, 35, 0.6)',
|
||||
}}
|
||||
>
|
||||
{/* 顶部金色光条装饰 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left="20px"
|
||||
right="20px"
|
||||
height="1px"
|
||||
background={`linear-gradient(90deg, transparent, ${T.gold}40, transparent)`}
|
||||
/>
|
||||
|
||||
{/* 标题行 + 时间选择器 */}
|
||||
<Flex justify="space-between" align="center" mb={3}>
|
||||
<Text
|
||||
fontSize="14px"
|
||||
fontWeight="700"
|
||||
color={T.gold}
|
||||
textTransform="uppercase"
|
||||
letterSpacing="0.1em"
|
||||
textShadow={`0 0 12px ${T.gold}60`}
|
||||
>
|
||||
主力动态
|
||||
</Text>
|
||||
|
||||
<ButtonGroup size="xs" spacing={1}>
|
||||
{PERIOD_OPTIONS.map(({ label }) => (
|
||||
<Button
|
||||
key={label}
|
||||
variant={selectedPeriod === label ? 'solid' : 'ghost'}
|
||||
bg={
|
||||
selectedPeriod === label
|
||||
? 'rgba(212, 175, 55, 0.2)'
|
||||
: 'transparent'
|
||||
}
|
||||
color={selectedPeriod === label ? T.gold : T.textMuted}
|
||||
border={
|
||||
selectedPeriod === label
|
||||
? `1px solid ${T.borderGold}`
|
||||
: '1px solid transparent'
|
||||
}
|
||||
_hover={{
|
||||
bg: 'rgba(212, 175, 55, 0.15)',
|
||||
color: T.gold,
|
||||
}}
|
||||
onClick={() => handlePeriodChange(label)}
|
||||
fontSize="11px"
|
||||
h="24px"
|
||||
px={2}
|
||||
borderRadius="6px"
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
|
||||
{/* 内容区域 */}
|
||||
{isLoading ? (
|
||||
<Flex justify="center" align="center" h="180px">
|
||||
<Spinner size="sm" color={T.gold} />
|
||||
</Flex>
|
||||
) : displayItems.length === 0 ? (
|
||||
<Flex justify="center" align="center" h="180px">
|
||||
<Text color={T.textMuted} fontSize="13px">
|
||||
暂无主力资金流数据
|
||||
</Text>
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex gap={4} flexDirection={{ base: 'column', md: 'row' }}>
|
||||
{/* 左侧:今日摘要 */}
|
||||
<TodaySummary latestItem={latestItem} />
|
||||
|
||||
{/* 右侧:时间序列图表 */}
|
||||
<Box flex={1} minH="180px">
|
||||
{chartOption && (
|
||||
<EChartsWrapper
|
||||
option={chartOption}
|
||||
style={{ height: '200px', width: '100%' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MainCapitalFlowSection.displayName = 'MainCapitalFlowSection';
|
||||
|
||||
export default MainCapitalFlowSection;
|
||||
@@ -14,6 +14,7 @@
|
||||
export { PriceDisplay } from './PriceDisplay';
|
||||
export { SecondaryQuote } from './SecondaryQuote';
|
||||
export { MainForceInfo } from './MainForceInfo';
|
||||
export { MainCapitalFlowSection } from './MainCapitalFlowSection';
|
||||
export { StockHeader } from './StockHeader';
|
||||
export { MetricRow } from './MetricRow';
|
||||
|
||||
@@ -46,6 +47,7 @@ export * from './formatters';
|
||||
export type { PriceDisplayProps } from './PriceDisplay';
|
||||
export type { SecondaryQuoteProps } from './SecondaryQuote';
|
||||
export type { MainForceInfoProps } from './MainForceInfo';
|
||||
export type { MainCapitalFlowSectionProps } from './MainCapitalFlowSection';
|
||||
export type { StockHeaderProps } from './StockHeader';
|
||||
export type { MetricRowProps } from './MetricRow';
|
||||
export type { GlassSectionProps } from './GlassSection';
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
|
||||
export { useStockQuoteData } from './useStockQuoteData';
|
||||
export { useStockCompare } from './useStockCompare';
|
||||
export { useMainCapitalFlow } from './useMainCapitalFlow';
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* useMainCapitalFlow - 主力资金流时间序列数据获取 Hook
|
||||
*
|
||||
* 从 /api/stock/{code}/main-capital-flow 接口获取历史资金流数据
|
||||
* 供 MainCapitalFlowSection 组件使用
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { logger } from '@utils/logger';
|
||||
import axios from '@utils/axiosConfig';
|
||||
import type { MainCapitalFlowItem } from '../types';
|
||||
|
||||
interface UseMainCapitalFlowResult {
|
||||
items: MainCapitalFlowItem[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 API 响应转换为前端数据格式
|
||||
*/
|
||||
const transformItems = (rawItems: any[]): MainCapitalFlowItem[] => {
|
||||
if (!Array.isArray(rawItems)) return [];
|
||||
return rawItems.map((item) => ({
|
||||
tradeDate: item.trade_date || item.tradeDate || '',
|
||||
netInflow: item.net_inflow ?? item.netInflow ?? 0,
|
||||
mainInflowRatio: item.main_inflow_ratio ?? item.mainInflowRatio ?? 0,
|
||||
netActiveBuyRatio: item.net_active_buy_ratio ?? item.netActiveBuyRatio ?? 0,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 主力资金流时间序列 Hook
|
||||
*
|
||||
* @param stockCode - 股票代码
|
||||
* @param days - 请求天数,默认 30
|
||||
*/
|
||||
export const useMainCapitalFlow = (
|
||||
stockCode?: string,
|
||||
days: number = 30,
|
||||
): UseMainCapitalFlowResult => {
|
||||
const [items, setItems] = useState<MainCapitalFlowItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async (signal?: AbortSignal) => {
|
||||
if (!stockCode) return;
|
||||
|
||||
const baseCode = stockCode.split('.')[0];
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
logger.debug('useMainCapitalFlow', '获取主力资金流时间序列', { stockCode, days });
|
||||
|
||||
try {
|
||||
const result = await axios.get(
|
||||
`/api/stock/${baseCode}/main-capital-flow`,
|
||||
{ params: { days }, signal },
|
||||
);
|
||||
|
||||
if (result.data.success && result.data.data?.items) {
|
||||
const transformed = transformItems(result.data.data.items);
|
||||
logger.debug('useMainCapitalFlow', '数据转换完成', {
|
||||
stockCode,
|
||||
count: transformed.length,
|
||||
});
|
||||
setItems(transformed);
|
||||
} else {
|
||||
setError('获取主力资金流数据失败');
|
||||
setItems([]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === 'CanceledError') return;
|
||||
logger.error('useMainCapitalFlow', '获取数据失败', err);
|
||||
setError('获取主力资金流数据失败');
|
||||
setItems([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [stockCode, days]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stockCode) {
|
||||
setItems([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
fetchData(controller.signal);
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [stockCode, days, fetchData]);
|
||||
|
||||
return {
|
||||
items,
|
||||
isLoading,
|
||||
error,
|
||||
refetch: () => fetchData(),
|
||||
};
|
||||
};
|
||||
|
||||
export default useMainCapitalFlow;
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
PriceDisplay,
|
||||
SecondaryQuote,
|
||||
MetricRow,
|
||||
MainForceInfo,
|
||||
MainCapitalFlowSection,
|
||||
DEEP_SPACE_THEME as T,
|
||||
formatPrice,
|
||||
} from './components';
|
||||
@@ -181,7 +181,7 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
|
||||
todayLow={displayData.todayLow}
|
||||
/>
|
||||
|
||||
{/* ========== 数据区块(三列布局)========== */}
|
||||
{/* ========== 数据区块(两列指标 + 全宽主力动态)========== */}
|
||||
<Flex gap={4} flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
|
||||
{/* 第一列:估值指标 - PE、流通股本、换手率 */}
|
||||
<GlassSection title="估值指标" flex={1}>
|
||||
@@ -223,17 +223,11 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
|
||||
/>
|
||||
</VStack>
|
||||
</GlassSection>
|
||||
|
||||
{/* 第三列:主力动态 */}
|
||||
<GlassSection title="主力动态" flex={1}>
|
||||
<MainForceInfo
|
||||
netInflow={quoteData.netInflow}
|
||||
mainInflowRatio={quoteData.mainInflowRatio}
|
||||
netActiveBuyRatio={quoteData.netActiveBuyRatio}
|
||||
/>
|
||||
</GlassSection>
|
||||
</Flex>
|
||||
|
||||
{/* ========== 主力动态(时间序列)========== */}
|
||||
<MainCapitalFlowSection stockCode={stockCode} />
|
||||
|
||||
{/* 公司信息区块已移至 CompanyOverview 模块 */}
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
@@ -45,6 +45,24 @@ export interface StockQuoteCardData {
|
||||
isFavorite?: boolean; // 是否已加入自选
|
||||
}
|
||||
|
||||
/**
|
||||
* 主力资金流单日数据(来自 stock_main_capital_flow 表)
|
||||
*/
|
||||
export interface MainCapitalFlowItem {
|
||||
tradeDate: string; // 交易日期 YYYY-MM-DD
|
||||
netInflow: number; // 主力净流入量(万元)
|
||||
mainInflowRatio: number; // 主力净流入量占比(%)
|
||||
netActiveBuyRatio: number; // 净主动买入额占比(%)
|
||||
}
|
||||
|
||||
/**
|
||||
* 主力资金流时间序列响应数据
|
||||
*/
|
||||
export interface MainCapitalFlowData {
|
||||
code: string;
|
||||
items: MainCapitalFlowItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* StockQuoteCard 组件 Props(优化后)
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user