feat: 添加相关股票模块
This commit is contained in:
@@ -747,6 +747,33 @@ export function generateMockEvents(params = {}) {
|
||||
const relatedAvgChg = (Math.random() * 20 - 5).toFixed(2); // -5% 到 15%
|
||||
const relatedMaxChg = (Math.random() * 30).toFixed(2); // 0% 到 30%
|
||||
|
||||
// 生成价格走势数据(前一天、当天、后一天)
|
||||
const generatePriceTrend = (seed) => {
|
||||
const basePrice = 10 + (seed % 90); // 基础价格 10-100
|
||||
const trend = [];
|
||||
|
||||
// 前一天(5个数据点)
|
||||
let price = basePrice;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
price = price + (Math.random() - 0.5) * 0.5;
|
||||
trend.push(parseFloat(price.toFixed(2)));
|
||||
}
|
||||
|
||||
// 当天(5个数据点)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
price = price + (Math.random() - 0.4) * 0.8; // 轻微上涨趋势
|
||||
trend.push(parseFloat(price.toFixed(2)));
|
||||
}
|
||||
|
||||
// 后一天(5个数据点)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
price = price + (Math.random() - 0.45) * 1.0;
|
||||
trend.push(parseFloat(price.toFixed(2)));
|
||||
}
|
||||
|
||||
return trend;
|
||||
};
|
||||
|
||||
// 为每个事件随机选择2-5个相关股票
|
||||
const relatedStockCount = 2 + (i % 4); // 2-5个股票
|
||||
const relatedStocks = [];
|
||||
@@ -758,10 +785,16 @@ export function generateMockEvents(params = {}) {
|
||||
for (let j = 0; j < Math.min(relatedStockCount, industryStocks.length); j++) {
|
||||
const stock = industryStocks[j % industryStocks.length];
|
||||
if (!addedStockCodes.has(stock.stock_code)) {
|
||||
const dailyChange = (Math.random() * 6 - 2).toFixed(2); // -2% ~ +4%
|
||||
const weekChange = (Math.random() * 10 - 3).toFixed(2); // -3% ~ +7%
|
||||
|
||||
relatedStocks.push({
|
||||
stock_name: stock.stock_name,
|
||||
stock_code: stock.stock_code,
|
||||
relation_desc: relationDescTemplates[(i + j) % relationDescTemplates.length]
|
||||
relation_desc: relationDescTemplates[(i + j) % relationDescTemplates.length],
|
||||
daily_change: dailyChange,
|
||||
week_change: weekChange,
|
||||
price_trend: generatePriceTrend(i * 100 + j)
|
||||
});
|
||||
addedStockCodes.add(stock.stock_code);
|
||||
}
|
||||
@@ -773,10 +806,16 @@ export function generateMockEvents(params = {}) {
|
||||
while (relatedStocks.length < relatedStockCount && poolIndex < stockPool.length) {
|
||||
const randomStock = stockPool[poolIndex % stockPool.length];
|
||||
if (!addedStockCodes.has(randomStock.stock_code)) {
|
||||
const dailyChange = (Math.random() * 6 - 2).toFixed(2); // -2% ~ +4%
|
||||
const weekChange = (Math.random() * 10 - 3).toFixed(2); // -3% ~ +7%
|
||||
|
||||
relatedStocks.push({
|
||||
stock_name: randomStock.stock_name,
|
||||
stock_code: randomStock.stock_code,
|
||||
relation_desc: relationDescTemplates[(i + poolIndex) % relationDescTemplates.length]
|
||||
relation_desc: relationDescTemplates[(i + poolIndex) % relationDescTemplates.length],
|
||||
daily_change: dailyChange,
|
||||
week_change: weekChange,
|
||||
price_trend: generatePriceTrend(i * 100 + poolIndex)
|
||||
});
|
||||
addedStockCodes.add(randomStock.stock_code);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
// src/views/Community/components/DynamicNewsDetail/MiniKLineChart.js
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
fetchKlineData,
|
||||
getCacheKey,
|
||||
klineDataCache
|
||||
} from '../StockDetailPanel/utils/klineDataCache';
|
||||
|
||||
/**
|
||||
* 迷你K线图组件
|
||||
* 显示股票的K线走势(蜡烛图),支持事件时间标记
|
||||
*
|
||||
* @param {string} stockCode - 股票代码
|
||||
* @param {string} eventTime - 事件时间(可选)
|
||||
* @param {Function} onClick - 点击回调(可选)
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const MiniKLineChart = React.memo(function MiniKLineChart({ stockCode, eventTime, onClick }) {
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const mountedRef = useRef(true);
|
||||
const loadedRef = useRef(false);
|
||||
const dataFetchedRef = useRef(false);
|
||||
|
||||
// 稳定的事件时间
|
||||
const stableEventTime = useMemo(() => {
|
||||
return eventTime ? moment(eventTime).format('YYYY-MM-DD HH:mm') : '';
|
||||
}, [eventTime]);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stockCode) {
|
||||
setData([]);
|
||||
loadedRef.current = false;
|
||||
dataFetchedRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataFetchedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查缓存(K线图使用 'daily' 类型)
|
||||
const cacheKey = getCacheKey(stockCode, stableEventTime, 'daily');
|
||||
const cachedData = klineDataCache.get(cacheKey);
|
||||
|
||||
if (cachedData && cachedData.length > 0) {
|
||||
setData(cachedData);
|
||||
loadedRef.current = true;
|
||||
dataFetchedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
dataFetchedRef.current = true;
|
||||
setLoading(true);
|
||||
|
||||
// 获取日K线数据
|
||||
fetchKlineData(stockCode, stableEventTime, 'daily')
|
||||
.then((result) => {
|
||||
if (mountedRef.current) {
|
||||
setData(result);
|
||||
setLoading(false);
|
||||
loadedRef.current = true;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (mountedRef.current) {
|
||||
setData([]);
|
||||
setLoading(false);
|
||||
loadedRef.current = true;
|
||||
}
|
||||
});
|
||||
}, [stockCode, stableEventTime]);
|
||||
|
||||
const chartOption = useMemo(() => {
|
||||
// 提取K线数据 [open, close, low, high]
|
||||
const klineData = data
|
||||
.filter(item => item.open && item.close && item.low && item.high)
|
||||
.map(item => [item.open, item.close, item.low, item.high]);
|
||||
|
||||
// 日K线使用 date 字段
|
||||
const dates = data.map(item => item.date || item.time);
|
||||
const hasData = klineData.length > 0;
|
||||
|
||||
if (!hasData) {
|
||||
return {
|
||||
title: {
|
||||
text: loading ? '加载中...' : '无数据',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: { color: '#999', fontSize: 10 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 计算事件时间标记
|
||||
let eventMarkLineData = [];
|
||||
if (stableEventTime && Array.isArray(dates) && dates.length > 0) {
|
||||
try {
|
||||
const eventDate = moment(stableEventTime).format('YYYY-MM-DD');
|
||||
const eventIdx = dates.findIndex(d => {
|
||||
const dateStr = typeof d === 'object' ? moment(d).format('YYYY-MM-DD') : String(d);
|
||||
return dateStr.includes(eventDate);
|
||||
});
|
||||
|
||||
if (eventIdx >= 0) {
|
||||
eventMarkLineData.push({
|
||||
xAxis: eventIdx,
|
||||
lineStyle: { color: '#FFD700', type: 'solid', width: 1.5 },
|
||||
label: { show: false }
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略异常
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
grid: { left: 2, right: 2, top: 2, bottom: 2, containLabel: false },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
show: false,
|
||||
boundaryGap: true
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
show: false,
|
||||
scale: true
|
||||
},
|
||||
series: [{
|
||||
type: 'candlestick',
|
||||
data: klineData,
|
||||
itemStyle: {
|
||||
color: '#ef5350', // 涨(阳线)
|
||||
color0: '#26a69a', // 跌(阴线)
|
||||
borderColor: '#ef5350', // 涨(边框)
|
||||
borderColor0: '#26a69a' // 跌(边框)
|
||||
},
|
||||
barWidth: '60%',
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
label: { show: false },
|
||||
data: eventMarkLineData
|
||||
}
|
||||
}],
|
||||
tooltip: { show: false },
|
||||
animation: false
|
||||
};
|
||||
}, [data, loading, stableEventTime]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 140,
|
||||
height: 40,
|
||||
cursor: onClick ? 'pointer' : 'default'
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<ReactECharts
|
||||
option={chartOption}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
return prevProps.stockCode === nextProps.stockCode &&
|
||||
prevProps.eventTime === nextProps.eventTime &&
|
||||
prevProps.onClick === nextProps.onClick;
|
||||
});
|
||||
|
||||
export default MiniKLineChart;
|
||||
@@ -0,0 +1,94 @@
|
||||
// src/views/Community/components/DynamicNewsDetail/MiniLineChart.js
|
||||
// Mini 折线图组件(用于股票卡片)
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* Mini 折线图组件
|
||||
* @param {Object} props
|
||||
* @param {Array<number>} props.data - 价格走势数据数组(15个数据点:前5+中5+后5)
|
||||
* @param {number} props.width - 图表宽度(默认180)
|
||||
* @param {number} props.height - 图表高度(默认60)
|
||||
*/
|
||||
const MiniLineChart = ({ data = [], width = 180, height = 60 }) => {
|
||||
if (!data || data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算最大值和最小值,用于归一化
|
||||
const max = Math.max(...data);
|
||||
const min = Math.min(...data);
|
||||
const range = max - min || 1; // 防止除以0
|
||||
|
||||
// 将数据点转换为 SVG 路径坐标
|
||||
const points = data.map((value, index) => {
|
||||
const x = (index / (data.length - 1)) * width;
|
||||
const y = height - ((value - min) / range) * height;
|
||||
return `${x.toFixed(2)},${y.toFixed(2)}`;
|
||||
});
|
||||
|
||||
// 构建 SVG 路径字符串
|
||||
const pathD = `M ${points.join(' L ')}`;
|
||||
|
||||
// 判断整体趋势(比较第一个和最后一个值)
|
||||
const isPositive = data[data.length - 1] >= data[0];
|
||||
const strokeColor = isPositive ? '#48BB78' : '#F56565'; // 绿色上涨,红色下跌
|
||||
|
||||
// 创建渐变填充区域路径
|
||||
const fillPathD = `${pathD} L ${width},${height} L 0,${height} Z`;
|
||||
|
||||
return (
|
||||
<Box width={`${width}px`} height={`${height}px`}>
|
||||
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
|
||||
<defs>
|
||||
<linearGradient id={`gradient-${isPositive ? 'up' : 'down'}`} x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stopColor={strokeColor} stopOpacity="0.3" />
|
||||
<stop offset="100%" stopColor={strokeColor} stopOpacity="0.05" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* 填充区域 */}
|
||||
<path
|
||||
d={fillPathD}
|
||||
fill={`url(#gradient-${isPositive ? 'up' : 'down'})`}
|
||||
/>
|
||||
|
||||
{/* 折线 */}
|
||||
<path
|
||||
d={pathD}
|
||||
fill="none"
|
||||
stroke={strokeColor}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
{/* 垂直分隔线(标记三个时间段) */}
|
||||
{/* 前一天和当天之间 */}
|
||||
<line
|
||||
x1={width / 3}
|
||||
y1={0}
|
||||
x2={width / 3}
|
||||
y2={height}
|
||||
stroke="#E2E8F0"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="2,2"
|
||||
/>
|
||||
|
||||
{/* 当天和后一天之间 */}
|
||||
<line
|
||||
x1={(width * 2) / 3}
|
||||
y1={0}
|
||||
x2={(width * 2) / 3}
|
||||
y2={height}
|
||||
stroke="#E2E8F0"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="2,2"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MiniLineChart;
|
||||
@@ -0,0 +1,66 @@
|
||||
// src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js
|
||||
// 相关股票列表区组件(可折叠,网格布局)
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
SimpleGrid,
|
||||
Collapse,
|
||||
} from '@chakra-ui/react';
|
||||
import CollapsibleHeader from './CollapsibleHeader';
|
||||
import StockListItem from './StockListItem';
|
||||
|
||||
/**
|
||||
* 相关股票列表区组件
|
||||
* @param {Object} props
|
||||
* @param {Array<Object>} props.stocks - 股票数组
|
||||
* @param {Object} props.quotes - 股票行情字典 { [stockCode]: { change: number } }
|
||||
* @param {string} props.eventTime - 事件时间
|
||||
* @param {Set} props.watchlistSet - 自选股代码集合
|
||||
* @param {boolean} props.isOpen - 是否展开
|
||||
* @param {Function} props.onToggle - 切换展开/收起的回调
|
||||
* @param {Function} props.onWatchlistToggle - 切换自选股回调
|
||||
*/
|
||||
const RelatedStocksSection = ({
|
||||
stocks,
|
||||
quotes = {},
|
||||
eventTime = null,
|
||||
watchlistSet = new Set(),
|
||||
isOpen,
|
||||
onToggle,
|
||||
onWatchlistToggle
|
||||
}) => {
|
||||
// 如果没有股票数据,不渲染
|
||||
if (!stocks || stocks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<CollapsibleHeader
|
||||
title="相关股票"
|
||||
isOpen={isOpen}
|
||||
onToggle={onToggle}
|
||||
count={stocks.length}
|
||||
/>
|
||||
<Collapse in={isOpen} animateOpacity>
|
||||
<Box mt={3}>
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
||||
{stocks.map((stock, index) => (
|
||||
<StockListItem
|
||||
key={index}
|
||||
stock={stock}
|
||||
quote={quotes[stock.stock_code]}
|
||||
eventTime={eventTime}
|
||||
isInWatchlist={watchlistSet.has(stock.stock_code)}
|
||||
onWatchlistToggle={onWatchlistToggle}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelatedStocksSection;
|
||||
@@ -0,0 +1,239 @@
|
||||
// src/views/Community/components/DynamicNewsDetail/StockListItem.js
|
||||
// 股票卡片组件(融合表格功能的卡片样式)
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
VStack,
|
||||
SimpleGrid,
|
||||
Text,
|
||||
Button,
|
||||
IconButton,
|
||||
Collapse,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { StarIcon } from '@chakra-ui/icons';
|
||||
import MiniTimelineChart from '../StockDetailPanel/components/MiniTimelineChart';
|
||||
import MiniKLineChart from './MiniKLineChart';
|
||||
import StockChartModal from '../../../../components/StockChart/StockChartModal';
|
||||
|
||||
/**
|
||||
* 股票卡片组件
|
||||
* @param {Object} props
|
||||
* @param {Object} props.stock - 股票对象
|
||||
* @param {string} props.stock.stock_name - 股票名称
|
||||
* @param {string} props.stock.stock_code - 股票代码
|
||||
* @param {string} props.stock.relation_desc - 关联描述
|
||||
* @param {Object} props.quote - 股票行情数据(可选)
|
||||
* @param {number} props.quote.change - 涨跌幅
|
||||
* @param {string} props.eventTime - 事件时间(可选)
|
||||
* @param {boolean} props.isInWatchlist - 是否在自选股中
|
||||
* @param {Function} props.onWatchlistToggle - 切换自选股回调
|
||||
*/
|
||||
const StockListItem = ({
|
||||
stock,
|
||||
quote = null,
|
||||
eventTime = null,
|
||||
isInWatchlist = false,
|
||||
onWatchlistToggle
|
||||
}) => {
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const codeColor = useColorModeValue('blue.600', 'blue.300');
|
||||
const nameColor = useColorModeValue('gray.700', 'gray.300');
|
||||
const descColor = useColorModeValue('gray.600', 'gray.400');
|
||||
const dividerColor = useColorModeValue('gray.200', 'gray.600');
|
||||
|
||||
const [isDescExpanded, setIsDescExpanded] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleViewDetail = () => {
|
||||
const stockCode = stock.stock_code.split('.')[0];
|
||||
window.open(`https://valuefrontier.cn/company?scode=${stockCode}`, '_blank');
|
||||
};
|
||||
|
||||
const handleWatchlistClick = (e) => {
|
||||
e.stopPropagation();
|
||||
onWatchlistToggle?.(stock.stock_code, isInWatchlist);
|
||||
};
|
||||
|
||||
// 格式化涨跌幅显示
|
||||
const formatChange = (value) => {
|
||||
if (value === null || value === undefined || isNaN(value)) return '--';
|
||||
const prefix = value > 0 ? '+' : '';
|
||||
return `${prefix}${parseFloat(value).toFixed(2)}%`;
|
||||
};
|
||||
|
||||
// 获取涨跌幅颜色
|
||||
const getChangeColor = (value) => {
|
||||
const num = parseFloat(value);
|
||||
if (isNaN(num) || num === 0) return 'gray.500';
|
||||
return num > 0 ? 'red.500' : 'green.500';
|
||||
};
|
||||
|
||||
// 获取涨跌幅数据(优先使用 quote,fallback 到 stock)
|
||||
const change = quote?.change ?? stock.daily_change ?? null;
|
||||
|
||||
// 处理关联描述
|
||||
const getRelationDesc = () => {
|
||||
const relationDesc = stock.relation_desc;
|
||||
|
||||
if (!relationDesc) return '--';
|
||||
|
||||
if (typeof relationDesc === 'string') {
|
||||
return relationDesc;
|
||||
} else if (typeof relationDesc === 'object' && relationDesc.data && Array.isArray(relationDesc.data)) {
|
||||
// 新格式:{data: [{query_part: "...", sentences: "..."}]}
|
||||
return relationDesc.data
|
||||
.map(item => item.query_part || item.sentences || '')
|
||||
.filter(s => s)
|
||||
.join(';') || '--';
|
||||
}
|
||||
|
||||
return '--';
|
||||
};
|
||||
|
||||
const relationText = getRelationDesc();
|
||||
const maxLength = 50; // 收缩时显示的最大字符数
|
||||
const needTruncate = relationText && relationText !== '--' && relationText.length > maxLength;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
p={4}
|
||||
_hover={{
|
||||
boxShadow: 'md',
|
||||
borderColor: 'blue.300',
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
{/* 顶部:股票代码 + 名称 + 操作按钮 */}
|
||||
<Flex justify="space-between" align="center">
|
||||
{/* 左侧:代码 + 名称 */}
|
||||
<Flex align="baseline" gap={2}>
|
||||
<Text
|
||||
fontSize="md"
|
||||
fontWeight="bold"
|
||||
color={codeColor}
|
||||
cursor="pointer"
|
||||
onClick={handleViewDetail}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
>
|
||||
{stock.stock_code}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={nameColor}>
|
||||
{stock.stock_name}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
fontWeight="semibold"
|
||||
color={getChangeColor(change)}
|
||||
>
|
||||
{formatChange(change)}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{/* 右侧:操作按钮 */}
|
||||
<Flex gap={2}>
|
||||
{onWatchlistToggle && (
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant={isInWatchlist ? 'solid' : 'outline'}
|
||||
colorScheme={isInWatchlist ? 'yellow' : 'gray'}
|
||||
icon={<StarIcon />}
|
||||
onClick={handleWatchlistClick}
|
||||
aria-label={isInWatchlist ? '已关注' : '加自选'}
|
||||
title={isInWatchlist ? '已关注' : '加自选'}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
onClick={handleViewDetail}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* 分隔线 */}
|
||||
<Box borderTop="1px solid" borderColor={dividerColor} />
|
||||
|
||||
{/* 分时图 & K线图 - 左右布局 */}
|
||||
<Box>
|
||||
<SimpleGrid columns={2} spacing={3}>
|
||||
{/* 左侧:分时图 */}
|
||||
<Box>
|
||||
<Text fontSize="xs" color={descColor} mb={1} textAlign="center">
|
||||
分时图
|
||||
</Text>
|
||||
<MiniTimelineChart
|
||||
stockCode={stock.stock_code}
|
||||
eventTime={eventTime}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 右侧:K线图 */}
|
||||
<Box>
|
||||
<Text fontSize="xs" color={descColor} mb={1} textAlign="center">
|
||||
日K线
|
||||
</Text>
|
||||
<MiniKLineChart
|
||||
stockCode={stock.stock_code}
|
||||
eventTime={eventTime}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
/>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
{/* 分隔线 */}
|
||||
<Box borderTop="1px solid" borderColor={dividerColor} />
|
||||
|
||||
{/* 关联描述 */}
|
||||
{relationText && relationText !== '--' && (
|
||||
<Box>
|
||||
<Text fontSize="xs" color={descColor} mb={1}>
|
||||
关联描述:
|
||||
</Text>
|
||||
<Collapse in={isDescExpanded} startingHeight={40}>
|
||||
<Text fontSize="sm" color={nameColor} lineHeight="1.6">
|
||||
{relationText}
|
||||
</Text>
|
||||
</Collapse>
|
||||
{needTruncate && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="link"
|
||||
colorScheme="blue"
|
||||
onClick={() => setIsDescExpanded(!isDescExpanded)}
|
||||
mt={1}
|
||||
>
|
||||
{isDescExpanded ? '收起' : '展开'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 股票详情弹窗 */}
|
||||
<StockChartModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
stock={stock}
|
||||
eventTime={eventTime}
|
||||
size="6xl"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockListItem;
|
||||
@@ -99,6 +99,30 @@ const Community = () => {
|
||||
const fetchDynamicNews = async () => {
|
||||
setDynamicNewsLoading(true);
|
||||
try {
|
||||
// 检查是否使用 mock 模式
|
||||
// 开发阶段默认使用 mock 数据
|
||||
const useMock = true; // TODO: 生产环境改为环境变量控制
|
||||
// const useMock = process.env.REACT_APP_USE_MOCK === 'true' ||
|
||||
// localStorage.getItem('use_mock_data') === 'true';
|
||||
|
||||
if (useMock) {
|
||||
// 使用 mock 数据
|
||||
const { generateMockEvents } = await import('../../mocks/data/events');
|
||||
const mockData = generateMockEvents({ page: 1, per_page: 30 });
|
||||
|
||||
// 调试:检查第一个事件的 related_stocks 数据
|
||||
if (mockData.events[0]?.related_stocks) {
|
||||
console.log('Mock 数据第一个事件的股票:', mockData.events[0].related_stocks);
|
||||
}
|
||||
|
||||
setDynamicNewsEvents(mockData.events);
|
||||
logger.info('Community', '动态新闻(Mock)加载成功', {
|
||||
count: mockData.events.length,
|
||||
mode: 'mock',
|
||||
firstEventStocks: mockData.events[0]?.related_stocks?.length || 0
|
||||
});
|
||||
} else {
|
||||
// 使用真实 API
|
||||
const timeRange = getCurrentTradingTimeRange();
|
||||
const response = await fetch(
|
||||
`/api/events/dynamic-news?start_time=${timeRange.startTime.toISOString()}&end_time=${timeRange.endTime.toISOString()}&count=30`,
|
||||
@@ -110,12 +134,14 @@ const Community = () => {
|
||||
setDynamicNewsEvents(data.data);
|
||||
logger.info('Community', '动态新闻加载成功', {
|
||||
count: data.data.length,
|
||||
timeRange: timeRange.description
|
||||
timeRange: timeRange.description,
|
||||
mode: 'api'
|
||||
});
|
||||
} else {
|
||||
logger.warn('Community', '动态新闻加载失败', data);
|
||||
setDynamicNewsEvents([]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Community', '动态新闻加载异常', error);
|
||||
setDynamicNewsEvents([]);
|
||||
|
||||
Reference in New Issue
Block a user