From e08b9d2104fb09497f40dd3c1ea71bea8966fe3e Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Tue, 16 Dec 2025 16:22:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor(DynamicTracking):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ForecastPanel: 业绩预告面板组件 - 新增 NewsPanel: 新闻面板组件 - 组件模块化重构 --- .../components/ForecastPanel.js | 97 +++++++++++++++ .../DynamicTracking/components/NewsPanel.js | 115 ++++++++++++++++++ .../DynamicTracking/components/index.js | 4 + 3 files changed, 216 insertions(+) create mode 100644 src/views/Company/components/DynamicTracking/components/ForecastPanel.js create mode 100644 src/views/Company/components/DynamicTracking/components/NewsPanel.js create mode 100644 src/views/Company/components/DynamicTracking/components/index.js diff --git a/src/views/Company/components/DynamicTracking/components/ForecastPanel.js b/src/views/Company/components/DynamicTracking/components/ForecastPanel.js new file mode 100644 index 00000000..91cadc8f --- /dev/null +++ b/src/views/Company/components/DynamicTracking/components/ForecastPanel.js @@ -0,0 +1,97 @@ +// src/views/Company/components/DynamicTracking/components/ForecastPanel.js +// 业绩预告面板 + +import React, { useState, useEffect, useCallback } from 'react'; +import { + VStack, + Card, + CardBody, + HStack, + Badge, + Text, + Spinner, + Center, +} from '@chakra-ui/react'; +import { logger } from '@utils/logger'; +import { getApiBase } from '@utils/apiConfig'; +import { THEME } from '../../CompanyOverview/BasicInfoTab/config'; + +const API_BASE_URL = getApiBase(); + +const ForecastPanel = ({ stockCode }) => { + const [forecast, setForecast] = useState(null); + const [loading, setLoading] = useState(false); + + const loadForecast = useCallback(async () => { + if (!stockCode) return; + + setLoading(true); + try { + const response = await fetch( + `${API_BASE_URL}/api/stock/${stockCode}/forecast` + ); + const result = await response.json(); + if (result.success && result.data) { + setForecast(result.data); + } + } catch (err) { + logger.error('ForecastPanel', 'loadForecast', err, { stockCode }); + setForecast(null); + } finally { + setLoading(false); + } + }, [stockCode]); + + useEffect(() => { + loadForecast(); + }, [loadForecast]); + + if (loading) { + return ( +
+ +
+ ); + } + + if (!forecast?.forecasts?.length) { + return ( +
+ 暂无业绩预告数据 +
+ ); + } + + return ( + + {forecast.forecasts.map((item, idx) => ( + + + + {item.forecast_type} + + 报告期: {item.report_date} + + + {item.content} + {item.reason && ( + + {item.reason} + + )} + {item.change_range?.lower && ( + + 预计变动范围: + + {item.change_range.lower}% ~ {item.change_range.upper}% + + + )} + + + ))} + + ); +}; + +export default ForecastPanel; diff --git a/src/views/Company/components/DynamicTracking/components/NewsPanel.js b/src/views/Company/components/DynamicTracking/components/NewsPanel.js new file mode 100644 index 00000000..a9990d4e --- /dev/null +++ b/src/views/Company/components/DynamicTracking/components/NewsPanel.js @@ -0,0 +1,115 @@ +// src/views/Company/components/DynamicTracking/components/NewsPanel.js +// 新闻动态面板(包装 NewsEventsTab) + +import React, { useState, useEffect, useCallback } from 'react'; +import { logger } from '@utils/logger'; +import { getApiBase } from '@utils/apiConfig'; +import NewsEventsTab from '../../CompanyOverview/NewsEventsTab'; + +const API_BASE_URL = getApiBase(); + +const NewsPanel = ({ stockCode }) => { + const [newsEvents, setNewsEvents] = useState([]); + const [loading, setLoading] = useState(false); + const [pagination, setPagination] = useState({ + page: 1, + per_page: 10, + total: 0, + pages: 0, + has_next: false, + has_prev: false, + }); + const [searchQuery, setSearchQuery] = useState(''); + const [stockName, setStockName] = useState(''); + + // 获取股票名称 + const fetchStockName = useCallback(async () => { + try { + const response = await fetch( + `${API_BASE_URL}/api/stock/${stockCode}/basic-info` + ); + const result = await response.json(); + if (result.success && result.data) { + const name = result.data.SECNAME || result.data.ORGNAME || stockCode; + setStockName(name); + return name; + } + return stockCode; + } catch (err) { + logger.error('NewsPanel', 'fetchStockName', err, { stockCode }); + return stockCode; + } + }, [stockCode]); + + // 加载新闻事件 + const loadNewsEvents = useCallback( + async (query, page = 1) => { + setLoading(true); + try { + const searchTerm = query || stockName || stockCode; + const response = await fetch( + `${API_BASE_URL}/api/events?q=${encodeURIComponent(searchTerm)}&page=${page}&per_page=10` + ); + const result = await response.json(); + + if (result.success) { + setNewsEvents(result.data || []); + setPagination({ + page: result.pagination?.page || page, + per_page: result.pagination?.per_page || 10, + total: result.pagination?.total || 0, + pages: result.pagination?.pages || 0, + has_next: result.pagination?.has_next || false, + has_prev: result.pagination?.has_prev || false, + }); + } + } catch (err) { + logger.error('NewsPanel', 'loadNewsEvents', err, { stockCode }); + setNewsEvents([]); + } finally { + setLoading(false); + } + }, + [stockCode, stockName] + ); + + // 首次加载 + useEffect(() => { + const initLoad = async () => { + if (stockCode) { + const name = await fetchStockName(); + await loadNewsEvents(name, 1); + } + }; + initLoad(); + }, [stockCode, fetchStockName, loadNewsEvents]); + + // 搜索处理 + const handleSearchChange = (value) => { + setSearchQuery(value); + }; + + const handleSearch = () => { + loadNewsEvents(searchQuery || stockName, 1); + }; + + // 分页处理 + const handlePageChange = (page) => { + loadNewsEvents(searchQuery || stockName, page); + }; + + return ( + + ); +}; + +export default NewsPanel; diff --git a/src/views/Company/components/DynamicTracking/components/index.js b/src/views/Company/components/DynamicTracking/components/index.js new file mode 100644 index 00000000..44bc24a1 --- /dev/null +++ b/src/views/Company/components/DynamicTracking/components/index.js @@ -0,0 +1,4 @@ +// src/views/Company/components/DynamicTracking/components/index.js + +export { default as NewsPanel } from './NewsPanel'; +export { default as ForecastPanel } from './ForecastPanel';