refactor(FinancialPanorama): 重构主组件为模块化结构
从 2,150 行单文件重构为模块化 TypeScript 组件: - 使用 useFinancialData Hook 管理数据加载 - 组合9个子组件渲染9个Tab面板 - 保留指标图表弹窗功能 - 保留期数选择器功能 - 删除旧的 index.js(2,150行) - 新增 index.tsx(454行,精简79%) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
453
src/views/Company/components/FinancialPanorama/index.tsx
Normal file
453
src/views/Company/components/FinancialPanorama/index.tsx
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
/**
|
||||||
|
* 财务全景组件
|
||||||
|
* 重构后的主组件,使用模块化结构
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, ReactNode } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardHeader,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Select,
|
||||||
|
IconButton,
|
||||||
|
Alert,
|
||||||
|
AlertIcon,
|
||||||
|
Skeleton,
|
||||||
|
Tabs,
|
||||||
|
TabList,
|
||||||
|
TabPanels,
|
||||||
|
Tab,
|
||||||
|
TabPanel,
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
useDisclosure,
|
||||||
|
Table,
|
||||||
|
Thead,
|
||||||
|
Tbody,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Td,
|
||||||
|
TableContainer,
|
||||||
|
Divider,
|
||||||
|
Tooltip,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { RepeatIcon } from '@chakra-ui/icons';
|
||||||
|
import ReactECharts from 'echarts-for-react';
|
||||||
|
import { formatUtils } from '@services/financialService';
|
||||||
|
|
||||||
|
// 内部模块导入
|
||||||
|
import { useFinancialData } from './hooks';
|
||||||
|
import { COLORS } from './constants';
|
||||||
|
import { calculateYoYChange, getCellBackground, getMetricChartOption } from './utils';
|
||||||
|
import {
|
||||||
|
StockInfoHeader,
|
||||||
|
BalanceSheetTable,
|
||||||
|
IncomeStatementTable,
|
||||||
|
CashflowTable,
|
||||||
|
FinancialMetricsTable,
|
||||||
|
MainBusinessAnalysis,
|
||||||
|
IndustryRankingView,
|
||||||
|
StockComparison,
|
||||||
|
ComparisonAnalysis,
|
||||||
|
} from './components';
|
||||||
|
import type { FinancialPanoramaProps } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 财务全景主组件
|
||||||
|
*/
|
||||||
|
const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propStockCode }) => {
|
||||||
|
// 使用数据加载 Hook
|
||||||
|
const {
|
||||||
|
stockInfo,
|
||||||
|
balanceSheet,
|
||||||
|
incomeStatement,
|
||||||
|
cashflow,
|
||||||
|
financialMetrics,
|
||||||
|
mainBusiness,
|
||||||
|
forecast,
|
||||||
|
industryRank,
|
||||||
|
comparison,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
currentStockCode,
|
||||||
|
selectedPeriods,
|
||||||
|
setSelectedPeriods,
|
||||||
|
} = useFinancialData({ stockCode: propStockCode });
|
||||||
|
|
||||||
|
// UI 状态
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const [modalContent, setModalContent] = useState<ReactNode>(null);
|
||||||
|
|
||||||
|
// 颜色配置
|
||||||
|
const { bgColor, hoverBg, positiveColor, negativeColor, borderColor } = COLORS;
|
||||||
|
|
||||||
|
// 点击指标行显示图表
|
||||||
|
const showMetricChart = (
|
||||||
|
metricName: string,
|
||||||
|
metricKey: string,
|
||||||
|
data: Array<{ period: string; [key: string]: unknown }>,
|
||||||
|
dataPath: string
|
||||||
|
) => {
|
||||||
|
const chartData = data
|
||||||
|
.map((item) => {
|
||||||
|
const value = dataPath.split('.').reduce((obj: unknown, key: string) => {
|
||||||
|
if (obj && typeof obj === 'object') {
|
||||||
|
return (obj as Record<string, unknown>)[key];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, item) as number | undefined;
|
||||||
|
return {
|
||||||
|
period: formatUtils.getReportType(item.period),
|
||||||
|
date: item.period,
|
||||||
|
value: value ?? 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.reverse();
|
||||||
|
|
||||||
|
const option = getMetricChartOption(metricName, chartData);
|
||||||
|
|
||||||
|
setModalContent(
|
||||||
|
<Box>
|
||||||
|
<ReactECharts option={option} style={{ height: '400px', width: '100%' }} />
|
||||||
|
<Divider my={4} />
|
||||||
|
<TableContainer>
|
||||||
|
<Table size="sm">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>报告期</Th>
|
||||||
|
<Th isNumeric>数值</Th>
|
||||||
|
<Th isNumeric>同比</Th>
|
||||||
|
<Th isNumeric>环比</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{chartData.map((item, idx) => {
|
||||||
|
// 计算环比
|
||||||
|
const qoq =
|
||||||
|
idx > 0
|
||||||
|
? ((item.value - chartData[idx - 1].value) /
|
||||||
|
Math.abs(chartData[idx - 1].value)) *
|
||||||
|
100
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 计算同比
|
||||||
|
const currentDate = new Date(item.date);
|
||||||
|
const lastYearItem = chartData.find((d) => {
|
||||||
|
const date = new Date(d.date);
|
||||||
|
return (
|
||||||
|
date.getFullYear() === currentDate.getFullYear() - 1 &&
|
||||||
|
date.getMonth() === currentDate.getMonth()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const yoy = lastYearItem
|
||||||
|
? ((item.value - lastYearItem.value) / Math.abs(lastYearItem.value)) * 100
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={idx}>
|
||||||
|
<Td>{item.period}</Td>
|
||||||
|
<Td isNumeric>{formatUtils.formatLargeNumber(item.value)}</Td>
|
||||||
|
<Td
|
||||||
|
isNumeric
|
||||||
|
color={
|
||||||
|
yoy !== null && yoy > 0
|
||||||
|
? positiveColor
|
||||||
|
: yoy !== null && yoy < 0
|
||||||
|
? negativeColor
|
||||||
|
: 'gray.500'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{yoy !== null ? `${yoy.toFixed(2)}%` : '-'}
|
||||||
|
</Td>
|
||||||
|
<Td
|
||||||
|
isNumeric
|
||||||
|
color={
|
||||||
|
qoq !== null && qoq > 0
|
||||||
|
? positiveColor
|
||||||
|
: qoq !== null && qoq < 0
|
||||||
|
? negativeColor
|
||||||
|
: 'gray.500'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{qoq !== null ? `${qoq.toFixed(2)}%` : '-'}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
onOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通用表格属性
|
||||||
|
const tableProps = {
|
||||||
|
showMetricChart,
|
||||||
|
calculateYoYChange,
|
||||||
|
getCellBackground,
|
||||||
|
positiveColor,
|
||||||
|
negativeColor,
|
||||||
|
bgColor,
|
||||||
|
hoverBg,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxW="container.xl" py={5}>
|
||||||
|
<VStack spacing={6} align="stretch">
|
||||||
|
{/* 时间选择器 */}
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<HStack>
|
||||||
|
<Text fontSize="sm" color="gray.600">
|
||||||
|
显示期数:
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
value={selectedPeriods}
|
||||||
|
onChange={(e) => setSelectedPeriods(Number(e.target.value))}
|
||||||
|
w="150px"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<option value={4}>最近4期</option>
|
||||||
|
<option value={8}>最近8期</option>
|
||||||
|
<option value={12}>最近12期</option>
|
||||||
|
<option value={16}>最近16期</option>
|
||||||
|
</Select>
|
||||||
|
</HStack>
|
||||||
|
<IconButton
|
||||||
|
icon={<RepeatIcon />}
|
||||||
|
onClick={refetch}
|
||||||
|
isLoading={loading}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
aria-label="刷新数据"
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 股票信息头部 */}
|
||||||
|
{loading ? (
|
||||||
|
<Skeleton height="150px" />
|
||||||
|
) : (
|
||||||
|
<StockInfoHeader
|
||||||
|
stockInfo={stockInfo}
|
||||||
|
positiveColor={positiveColor}
|
||||||
|
negativeColor={negativeColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
{!loading && stockInfo && (
|
||||||
|
<Tabs
|
||||||
|
index={activeTab}
|
||||||
|
onChange={setActiveTab}
|
||||||
|
variant="enclosed"
|
||||||
|
colorScheme="blue"
|
||||||
|
>
|
||||||
|
<TabList>
|
||||||
|
<Tab>财务概览</Tab>
|
||||||
|
<Tab>资产负债表</Tab>
|
||||||
|
<Tab>利润表</Tab>
|
||||||
|
<Tab>现金流量表</Tab>
|
||||||
|
<Tab>财务指标</Tab>
|
||||||
|
<Tab>主营业务</Tab>
|
||||||
|
<Tab>行业排名</Tab>
|
||||||
|
<Tab>业绩预告</Tab>
|
||||||
|
<Tab>股票对比</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanels>
|
||||||
|
{/* 财务概览 */}
|
||||||
|
<TabPanel>
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
<ComparisonAnalysis comparison={comparison} />
|
||||||
|
<FinancialMetricsTable data={financialMetrics} {...tableProps} />
|
||||||
|
</VStack>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 资产负债表 */}
|
||||||
|
<TabPanel>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<VStack align="stretch" spacing={2}>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Heading size="md">资产负债表</Heading>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Badge colorScheme="blue">
|
||||||
|
显示最近{Math.min(balanceSheet.length, 8)}期
|
||||||
|
</Badge>
|
||||||
|
<Text fontSize="sm" color="gray.500">
|
||||||
|
红涨绿跌 | 同比变化
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
提示:表格可横向滚动查看更多数据,点击行查看历史趋势
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
<BalanceSheetTable data={balanceSheet} {...tableProps} />
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 利润表 */}
|
||||||
|
<TabPanel>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<VStack align="stretch" spacing={2}>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Heading size="md">利润表</Heading>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Badge colorScheme="blue">
|
||||||
|
显示最近{Math.min(incomeStatement.length, 8)}期
|
||||||
|
</Badge>
|
||||||
|
<Text fontSize="sm" color="gray.500">
|
||||||
|
红涨绿跌 | 同比变化
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
提示:Q1、中报、Q3、年报数据为累计值,同比显示与去年同期对比
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
<IncomeStatementTable data={incomeStatement} {...tableProps} />
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 现金流量表 */}
|
||||||
|
<TabPanel>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<VStack align="stretch" spacing={2}>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Heading size="md">现金流量表</Heading>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Badge colorScheme="blue">
|
||||||
|
显示最近{Math.min(cashflow.length, 8)}期
|
||||||
|
</Badge>
|
||||||
|
<Text fontSize="sm" color="gray.500">
|
||||||
|
红涨绿跌 | 同比变化
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
提示:现金流数据为累计值,正值红色表示现金流入,负值绿色表示现金流出
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
<CashflowTable data={cashflow} {...tableProps} />
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 财务指标 */}
|
||||||
|
<TabPanel>
|
||||||
|
<FinancialMetricsTable data={financialMetrics} {...tableProps} />
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 主营业务 */}
|
||||||
|
<TabPanel>
|
||||||
|
<MainBusinessAnalysis mainBusiness={mainBusiness} />
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 行业排名 */}
|
||||||
|
<TabPanel>
|
||||||
|
<IndustryRankingView
|
||||||
|
industryRank={industryRank}
|
||||||
|
bgColor={bgColor}
|
||||||
|
borderColor={borderColor}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 业绩预告 */}
|
||||||
|
<TabPanel>
|
||||||
|
{forecast && (
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
{forecast.forecasts?.map((item, idx) => (
|
||||||
|
<Card key={idx}>
|
||||||
|
<CardBody>
|
||||||
|
<HStack justify="space-between" mb={2}>
|
||||||
|
<Badge colorScheme="blue">{item.forecast_type}</Badge>
|
||||||
|
<Text fontSize="sm" color="gray.500">
|
||||||
|
报告期: {item.report_date}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text mb={2}>{item.content}</Text>
|
||||||
|
{item.reason && (
|
||||||
|
<Text fontSize="sm" color="gray.600">
|
||||||
|
{item.reason}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{item.change_range?.lower && (
|
||||||
|
<HStack mt={2}>
|
||||||
|
<Text fontSize="sm">预计变动范围:</Text>
|
||||||
|
<Badge colorScheme="green">
|
||||||
|
{item.change_range.lower}% ~ {item.change_range.upper}%
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 股票对比 */}
|
||||||
|
<TabPanel>
|
||||||
|
<StockComparison
|
||||||
|
currentStock={currentStockCode}
|
||||||
|
stockInfo={stockInfo}
|
||||||
|
positiveColor={positiveColor}
|
||||||
|
negativeColor={negativeColor}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 错误提示 */}
|
||||||
|
{error && (
|
||||||
|
<Alert status="error">
|
||||||
|
<AlertIcon />
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 弹出模态框 */}
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent maxW="900px">
|
||||||
|
<ModalHeader>指标详情</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody pb={6}>{modalContent}</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</VStack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinancialPanorama;
|
||||||
Reference in New Issue
Block a user