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';