refactor(DynamicTracking): 拆分组件
- 新增 ForecastPanel: 业绩预告面板组件 - 新增 NewsPanel: 新闻面板组件 - 组件模块化重构
This commit is contained in:
@@ -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 (
|
||||||
|
<Center py={10}>
|
||||||
|
<Spinner size="lg" color={THEME.gold} />
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forecast?.forecasts?.length) {
|
||||||
|
return (
|
||||||
|
<Center py={10}>
|
||||||
|
<Text color={THEME.textSecondary}>暂无业绩预告数据</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
{forecast.forecasts.map((item, idx) => (
|
||||||
|
<Card key={idx} bg={THEME.cardBg} borderColor={THEME.border} borderWidth="1px">
|
||||||
|
<CardBody>
|
||||||
|
<HStack justify="space-between" mb={2}>
|
||||||
|
<Badge colorScheme="blue">{item.forecast_type}</Badge>
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary}>
|
||||||
|
报告期: {item.report_date}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text mb={2} color={THEME.text}>{item.content}</Text>
|
||||||
|
{item.reason && (
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary}>
|
||||||
|
{item.reason}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{item.change_range?.lower && (
|
||||||
|
<HStack mt={2}>
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary}>预计变动范围:</Text>
|
||||||
|
<Badge colorScheme="green">
|
||||||
|
{item.change_range.lower}% ~ {item.change_range.upper}%
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForecastPanel;
|
||||||
@@ -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 (
|
||||||
|
<NewsEventsTab
|
||||||
|
newsEvents={newsEvents}
|
||||||
|
newsLoading={loading}
|
||||||
|
newsPagination={pagination}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
onSearchChange={handleSearchChange}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
cardBg="white"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsPanel;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// src/views/Company/components/DynamicTracking/components/index.js
|
||||||
|
|
||||||
|
export { default as NewsPanel } from './NewsPanel';
|
||||||
|
export { default as ForecastPanel } from './ForecastPanel';
|
||||||
Reference in New Issue
Block a user