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