From 3b8b749eb1e0550cf3654bc2eefe960394113809 Mon Sep 17 00:00:00 2001
From: zdl <3489966805@qq.com>
Date: Sat, 1 Nov 2025 12:19:47 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=9B=B8=E5=85=B3?=
=?UTF-8?q?=E8=82=A1=E7=A5=A8=E6=A8=A1=E5=9D=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/mocks/data/events.js | 43 +++-
.../DynamicNewsDetail/MiniKLineChart.js | 184 ++++++++++++++
.../DynamicNewsDetail/MiniLineChart.js | 94 +++++++
.../DynamicNewsDetail/RelatedStocksSection.js | 66 +++++
.../DynamicNewsDetail/StockListItem.js | 239 ++++++++++++++++++
src/views/Community/index.js | 52 +++-
6 files changed, 663 insertions(+), 15 deletions(-)
create mode 100644 src/views/Community/components/DynamicNewsDetail/MiniKLineChart.js
create mode 100644 src/views/Community/components/DynamicNewsDetail/MiniLineChart.js
create mode 100644 src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js
create mode 100644 src/views/Community/components/DynamicNewsDetail/StockListItem.js
diff --git a/src/mocks/data/events.js b/src/mocks/data/events.js
index 959f3d0d..652de263 100644
--- a/src/mocks/data/events.js
+++ b/src/mocks/data/events.js
@@ -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);
}
diff --git a/src/views/Community/components/DynamicNewsDetail/MiniKLineChart.js b/src/views/Community/components/DynamicNewsDetail/MiniKLineChart.js
new file mode 100644
index 00000000..a12324ea
--- /dev/null
+++ b/src/views/Community/components/DynamicNewsDetail/MiniKLineChart.js
@@ -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 (
+
+
+
+ );
+}, (prevProps, nextProps) => {
+ return prevProps.stockCode === nextProps.stockCode &&
+ prevProps.eventTime === nextProps.eventTime &&
+ prevProps.onClick === nextProps.onClick;
+});
+
+export default MiniKLineChart;
diff --git a/src/views/Community/components/DynamicNewsDetail/MiniLineChart.js b/src/views/Community/components/DynamicNewsDetail/MiniLineChart.js
new file mode 100644
index 00000000..1d869273
--- /dev/null
+++ b/src/views/Community/components/DynamicNewsDetail/MiniLineChart.js
@@ -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} 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 (
+
+
+
+ );
+};
+
+export default MiniLineChart;
diff --git a/src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js b/src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js
new file mode 100644
index 00000000..fc1bd53a
--- /dev/null
+++ b/src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js
@@ -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