feat(DynamicTracking): 新增业绩预告Tab

- 新增 forecast Tab(从 FinancialPanorama 迁移)
 - 新增 loadForecast 数据加载逻辑
 - 新增业绩预告列表展示

 🤖 Generated with [Claude Code](https://claude.com/claude-code)

 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-16 16:13:49 +08:00
parent 6738a09e3a
commit 804de885e1

View File

@@ -1,204 +1,65 @@
// src/views/Company/components/DynamicTracking/index.js // src/views/Company/components/DynamicTracking/index.js
// 动态跟踪 - 独立一级 Tab 组件(包含新闻动态等二级 Tab // 动态跟踪 - 独立一级 Tab 组件(包含新闻动态等二级 Tab
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useMemo } from 'react';
import { import { Box } from '@chakra-ui/react';
Box, import { FaNewspaper, FaBullhorn, FaCalendarAlt, FaChartBar } from 'react-icons/fa';
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
} from "@chakra-ui/react";
import { FaNewspaper, FaBullhorn, FaCalendarAlt } from "react-icons/fa";
import { logger } from "@utils/logger"; import SubTabContainer from '@components/SubTabContainer';
import { getApiBase } from "@utils/apiConfig"; import AnnouncementsPanel from '../CompanyOverview/BasicInfoTab/components/AnnouncementsPanel';
import NewsEventsTab from "../CompanyOverview/NewsEventsTab"; import DisclosureSchedulePanel from '../CompanyOverview/BasicInfoTab/components/DisclosureSchedulePanel';
import AnnouncementsPanel from "../CompanyOverview/BasicInfoTab/components/AnnouncementsPanel"; import { NewsPanel, ForecastPanel } from './components';
import DisclosureSchedulePanel from "../CompanyOverview/BasicInfoTab/components/DisclosureSchedulePanel";
import { THEME } from "../CompanyOverview/BasicInfoTab/config";
// API配置
const API_BASE_URL = getApiBase();
// 二级 Tab 配置 // 二级 Tab 配置
const TRACKING_TABS = [ const TRACKING_TABS = [
{ key: "news", name: "新闻动态", icon: FaNewspaper }, { key: 'news', name: '新闻动态', icon: FaNewspaper, component: NewsPanel },
{ key: "announcements", name: "公司公告", icon: FaBullhorn }, { key: 'announcements', name: '公司公告', icon: FaBullhorn, component: AnnouncementsPanel },
{ key: "disclosure", name: "财报披露日程", icon: FaCalendarAlt }, { key: 'disclosure', name: '财报披露日程', icon: FaCalendarAlt, component: DisclosureSchedulePanel },
{ key: 'forecast', name: '业绩预告', icon: FaChartBar, component: ForecastPanel },
]; ];
/** /**
* 动态跟踪组件 * 动态跟踪组件
* *
* 功能: * 功能:
* - 二级 Tab 结构 * - 使用 SubTabContainer 实现二级导航
* - Tab1: 新闻动态(复用 NewsEventsTab * - Tab1: 新闻动态
* - 预留后续扩展 * - Tab2: 公司公告
* - Tab3: 财报披露日程
* - Tab4: 业绩预告
* *
* @param {Object} props * @param {Object} props
* @param {string} props.stockCode - 股票代码 * @param {string} props.stockCode - 股票代码
*/ */
const DynamicTracking = ({ stockCode: propStockCode }) => { const DynamicTracking = ({ stockCode: propStockCode }) => {
const [stockCode, setStockCode] = useState(propStockCode || "000001"); const [stockCode, setStockCode] = useState(propStockCode || '000001');
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
// 新闻动态状态
const [newsEvents, setNewsEvents] = useState([]);
const [newsLoading, setNewsLoading] = useState(false);
const [newsPagination, setNewsPagination] = 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 [dataLoaded, setDataLoaded] = useState(false);
// 监听 props 中的 stockCode 变化 // 监听 props 中的 stockCode 变化
useEffect(() => { useEffect(() => {
if (propStockCode && propStockCode !== stockCode) { if (propStockCode && propStockCode !== stockCode) {
setStockCode(propStockCode); setStockCode(propStockCode);
setDataLoaded(false);
setNewsEvents([]);
setStockName("");
setSearchQuery("");
} }
}, [propStockCode, stockCode]); }, [propStockCode, stockCode]);
// 获取股票名称(用于搜索) // 传递给子组件的 props
const fetchStockName = useCallback(async () => { const componentProps = useMemo(
try { () => ({
const response = await fetch( stockCode,
`${API_BASE_URL}/api/stock/${stockCode}/basic-info` }),
); [stockCode]
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("DynamicTracking", "fetchStockName", err, { stockCode });
return stockCode;
}
}, [stockCode]);
// 加载新闻事件数据
const loadNewsEvents = useCallback(
async (query, page = 1) => {
setNewsLoading(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 || []);
setNewsPagination({
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("DynamicTracking", "loadNewsEvents", err, { stockCode });
setNewsEvents([]);
} finally {
setNewsLoading(false);
}
},
[stockCode, stockName]
); );
// 首次加载
useEffect(() => {
const initLoad = async () => {
if (stockCode && !dataLoaded) {
const name = await fetchStockName();
await loadNewsEvents(name, 1);
setDataLoaded(true);
}
};
initLoad();
}, [stockCode, dataLoaded, fetchStockName, loadNewsEvents]);
// 搜索处理
const handleSearchChange = (value) => {
setSearchQuery(value);
};
const handleSearch = () => {
loadNewsEvents(searchQuery || stockName, 1);
};
// 分页处理
const handlePageChange = (page) => {
loadNewsEvents(searchQuery || stockName, page);
};
return ( return (
<Box bg={THEME.bg} p={4} borderRadius="md"> <Box>
<Tabs <SubTabContainer
variant="soft-rounded" tabs={TRACKING_TABS}
componentProps={componentProps}
themePreset="blackGold"
index={activeTab} index={activeTab}
onChange={setActiveTab} onTabChange={(index) => setActiveTab(index)}
isLazy isLazy
> />
<TabList bg={THEME.cardBg} borderBottom="1px solid" borderColor={THEME.border}>
{TRACKING_TABS.map((tab) => (
<Tab
key={tab.key}
fontWeight="medium"
color={THEME.textSecondary}
_selected={{
color: THEME.tabSelected.color,
bg: THEME.tabSelected.bg,
borderRadius: "md",
}}
_hover={{ color: THEME.gold }}
>
{tab.name}
</Tab>
))}
</TabList>
<TabPanels>
{/* 新闻动态 Tab */}
<TabPanel p={4}>
<NewsEventsTab
newsEvents={newsEvents}
newsLoading={newsLoading}
newsPagination={newsPagination}
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
onSearch={handleSearch}
onPageChange={handlePageChange}
cardBg="white"
/>
</TabPanel>
{/* 公司公告 Tab */}
<TabPanel p={4}>
<AnnouncementsPanel stockCode={stockCode} />
</TabPanel>
{/* 财报披露日程 Tab */}
<TabPanel p={4}>
<DisclosureSchedulePanel stockCode={stockCode} />
</TabPanel>
</TabPanels>
</Tabs>
</Box> </Box>
); );
}; };