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:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user