refactor: Company 目录结构重组 - Tab 内容组件文件夹化

- 将 4 个 Tab 内容组件移动到 components/ 目录下
  - CompanyOverview.js → components/CompanyOverview/index.js
  - MarketDataView.js → components/MarketDataView/index.js
  - FinancialPanorama.js → components/FinancialPanorama/index.js
  - ForecastReport.js → components/ForecastReport/index.js
- 更新 CompanyTabs/index.js 导入路径
- 更新 routes/lazy-components.js 路由路径
- 修复组件内相对路径导入,改用 @utils/@services 别名

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-09 15:31:58 +08:00
parent 91bd581a5e
commit 4954c58525
7 changed files with 88 additions and 343 deletions

View File

@@ -35,9 +35,9 @@ export const lazyComponents = {
// 公司相关模块 // 公司相关模块
CompanyIndex: React.lazy(() => import('@views/Company')), CompanyIndex: React.lazy(() => import('@views/Company')),
ForecastReport: React.lazy(() => import('@views/Company/ForecastReport')), ForecastReport: React.lazy(() => import('@views/Company/components/ForecastReport')),
FinancialPanorama: React.lazy(() => import('@views/Company/FinancialPanorama')), FinancialPanorama: React.lazy(() => import('@views/Company/components/FinancialPanorama')),
MarketDataView: React.lazy(() => import('@views/Company/MarketDataView')), MarketDataView: React.lazy(() => import('@views/Company/components/MarketDataView')),
// Agent模块 // Agent模块
AgentChat: React.lazy(() => import('@views/AgentChat')), AgentChat: React.lazy(() => import('@views/AgentChat')),

View File

@@ -36,8 +36,8 @@ import {
} from '@chakra-ui/icons'; } from '@chakra-ui/icons';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import { logger } from '../../utils/logger'; import { logger } from '@utils/logger';
import { getApiBase } from '../../utils/apiConfig'; import { getApiBase } from '@utils/apiConfig';
// API配置 // API配置
const API_BASE_URL = getApiBase(); const API_BASE_URL = getApiBase();

View File

@@ -15,11 +15,11 @@ import {
import TabNavigation from './TabNavigation'; import TabNavigation from './TabNavigation';
import { COMPANY_TABS, getTabNameByIndex } from '../../constants'; import { COMPANY_TABS, getTabNameByIndex } from '../../constants';
// 子组件导入 // 子组件导入Tab 内容组件)
import FinancialPanorama from '../../FinancialPanorama'; import CompanyOverview from '../CompanyOverview';
import ForecastReport from '../../ForecastReport'; import MarketDataView from '../MarketDataView';
import MarketDataView from '../../MarketDataView'; import FinancialPanorama from '../FinancialPanorama';
import CompanyOverview from '../../CompanyOverview'; import ForecastReport from '../ForecastReport';
/** /**
* Tab 组件映射 * Tab 组件映射

View File

@@ -1,6 +1,6 @@
// src/views/Company/FinancialPanorama.jsx // src/views/Company/FinancialPanorama.jsx
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { logger } from '../../utils/logger'; import { logger } from '@utils/logger';
import { import {
Box, Box,
Container, Container,
@@ -75,7 +75,7 @@ import {
ArrowDownIcon, ArrowDownIcon,
} from '@chakra-ui/icons'; } from '@chakra-ui/icons';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import { financialService, formatUtils, chartUtils } from '../../services/financialService'; import { financialService, formatUtils, chartUtils } from '@services/financialService';
const FinancialPanorama = ({ stockCode: propStockCode }) => { const FinancialPanorama = ({ stockCode: propStockCode }) => {
// 状态管理 // 状态管理

View File

@@ -4,7 +4,7 @@ import { Box, Flex, Input, Button, SimpleGrid, HStack, Text, Skeleton, VStack }
import { Card, CardHeader, CardBody, Heading, Table, Thead, Tr, Th, Tbody, Td, Tag } from '@chakra-ui/react'; import { Card, CardHeader, CardBody, Heading, Table, Thead, Tr, Th, Tbody, Td, Tag } from '@chakra-ui/react';
import { RepeatIcon } from '@chakra-ui/icons'; import { RepeatIcon } from '@chakra-ui/icons';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import { stockService } from '../../services/eventService'; import { stockService } from '@services/eventService';
const ForecastReport = ({ stockCode: propStockCode }) => { const ForecastReport = ({ stockCode: propStockCode }) => {
const [code, setCode] = useState(propStockCode || '600000'); const [code, setCode] = useState(propStockCode || '600000');

View File

@@ -1,7 +1,7 @@
// src/views/Market/MarketDataPro.jsx // src/views/Market/MarketDataPro.jsx
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { logger } from '../../utils/logger'; import { logger } from '@utils/logger';
import { getApiBase } from '../../utils/apiConfig'; import { getApiBase } from '@utils/apiConfig';
import { import {
Box, Box,
Container, Container,

View File

@@ -1,51 +1,40 @@
import React, { useState, useEffect, useCallback } from 'react'; // src/views/Company/index.js
import { useSearchParams } from 'react-router-dom'; // 公司详情页面入口 - 纯组合层
import {
Container, import React, { useEffect, useRef } from 'react';
Heading, import { Container, VStack, useColorModeValue } from '@chakra-ui/react';
Card,
CardBody, // 自定义 Hooks
Tabs, import { useCompanyStock } from './hooks/useCompanyStock';
TabList, import { useCompanyWatchlist } from './hooks/useCompanyWatchlist';
TabPanels,
Tab,
TabPanel,
HStack,
VStack,
Input,
Button,
InputGroup,
InputLeftElement,
Text,
Badge,
Divider,
Icon,
useColorModeValue,
useColorMode,
IconButton,
useToast,
} from '@chakra-ui/react';
import { SearchIcon, MoonIcon, SunIcon, StarIcon } from '@chakra-ui/icons';
import { FaChartLine, FaMoneyBillWave, FaChartBar, FaInfoCircle } from 'react-icons/fa';
import { useAuth } from '../../contexts/AuthContext';
import { logger } from '../../utils/logger';
import { getApiBase } from '../../utils/apiConfig';
import FinancialPanorama from './FinancialPanorama';
import ForecastReport from './ForecastReport';
import MarketDataView from './MarketDataView';
import CompanyOverview from './CompanyOverview';
// 导入 PostHog 追踪 Hook
import { useCompanyEvents } from './hooks/useCompanyEvents'; import { useCompanyEvents } from './hooks/useCompanyEvents';
const CompanyIndex = () => { // 页面组件
const [searchParams, setSearchParams] = useSearchParams(); import CompanyHeader from './components/CompanyHeader';
const [stockCode, setStockCode] = useState(searchParams.get('scode') || '000001'); import CompanyTabs from './components/CompanyTabs';
const [inputCode, setInputCode] = useState(stockCode);
const { colorMode, toggleColorMode } = useColorMode();
const toast = useToast();
const { isAuthenticated } = useAuth();
// 🎯 PostHog 事件追踪 /**
* 公司详情页面
*
* 功能:
* - 股票搜索与代码管理
* - 自选股添加/移除
* - 多维度数据展示(概览、行情、财务、预测)
* - PostHog 事件追踪
*/
const CompanyIndex = () => {
const bgColor = useColorModeValue('white', 'gray.800');
// 1. 先获取股票代码(不带追踪回调)
const {
stockCode,
inputCode,
setInputCode,
handleSearch,
handleKeyPress,
} = useCompanyStock();
// 2. 再初始化事件追踪(传入 stockCode
const { const {
trackStockSearched, trackStockSearched,
trackTabChanged, trackTabChanged,
@@ -53,297 +42,53 @@ const CompanyIndex = () => {
trackWatchlistRemoved, trackWatchlistRemoved,
} = useCompanyEvents({ stockCode }); } = useCompanyEvents({ stockCode });
// Tab 索引状态(用于追踪 Tab 切换) // 3. 自选股管理
const [currentTabIndex, setCurrentTabIndex] = useState(0); const {
isInWatchlist,
isLoading: isWatchlistLoading,
toggle: handleWatchlistToggle,
} = useCompanyWatchlist({
stockCode,
tracking: {
onAdd: trackWatchlistAdded,
onRemove: trackWatchlistRemoved,
},
});
const bgColor = useColorModeValue('white', 'gray.800'); // 4. 监听 stockCode 变化,触发搜索追踪
const tabBg = useColorModeValue('gray.50', 'gray.700'); const prevStockCodeRef = useRef(stockCode);
const activeBg = useColorModeValue('blue.500', 'blue.400');
const [isInWatchlist, setIsInWatchlist] = useState(false);
const [isWatchlistLoading, setIsWatchlistLoading] = useState(false);
const loadWatchlistStatus = useCallback(async () => {
try {
const base = getApiBase();
const resp = await fetch(base + '/api/account/watchlist', {
credentials: 'include',
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
});
if (!resp.ok) {
setIsInWatchlist(false);
return;
}
const data = await resp.json();
const list = Array.isArray(data?.data) ? data.data : [];
const codes = new Set(list.map((item) => item.stock_code));
setIsInWatchlist(codes.has(stockCode));
} catch (e) {
setIsInWatchlist(false);
}
}, [stockCode]);
// 当URL参数变化时更新股票代码
useEffect(() => { useEffect(() => {
const scode = searchParams.get('scode'); if (stockCode !== prevStockCodeRef.current) {
if (scode && scode !== stockCode) { trackStockSearched(stockCode, prevStockCodeRef.current);
setStockCode(scode); prevStockCodeRef.current = stockCode;
setInputCode(scode);
} }
}, [searchParams, stockCode]); }, [stockCode, trackStockSearched]);
useEffect(() => {
loadWatchlistStatus();
}, [loadWatchlistStatus]);
const handleSearch = () => {
if (inputCode && inputCode !== stockCode) {
// 🎯 追踪股票搜索
trackStockSearched(inputCode, stockCode);
setStockCode(inputCode);
setSearchParams({ scode: inputCode });
}
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
handleSearch();
}
};
const handleWatchlistToggle = async () => {
if (!stockCode) {
logger.warn('CompanyIndex', 'handleWatchlistToggle', '无效的股票代码', { stockCode });
toast({ title: '无效的股票代码', status: 'error', duration: 2000 });
return;
}
if (!isAuthenticated) {
logger.warn('CompanyIndex', 'handleWatchlistToggle', '用户未登录', { stockCode });
toast({ title: '请先登录后再加入自选', status: 'warning', duration: 2000 });
return;
}
try {
setIsWatchlistLoading(true);
const base = getApiBase();
if (isInWatchlist) {
logger.debug('CompanyIndex', '准备从自选移除', { stockCode });
const url = base + `/api/account/watchlist/${stockCode}`;
logger.api.request('DELETE', url, { stockCode });
const resp = await fetch(url, {
method: 'DELETE',
credentials: 'include'
});
logger.api.response('DELETE', url, resp.status);
if (!resp.ok) throw new Error('删除失败');
// 🎯 追踪移除自选
trackWatchlistRemoved(stockCode);
setIsInWatchlist(false);
toast({ title: '已从自选移除', status: 'info', duration: 1500 });
} else {
logger.debug('CompanyIndex', '准备添加到自选', { stockCode });
const url = base + '/api/account/watchlist';
const body = { stock_code: stockCode };
logger.api.request('POST', url, body);
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(body)
});
logger.api.response('POST', url, resp.status);
if (!resp.ok) throw new Error('添加失败');
// 🎯 追踪加入自选
trackWatchlistAdded(stockCode);
setIsInWatchlist(true);
toast({ title: '已加入自选', status: 'success', duration: 1500 });
}
} catch (error) {
logger.error('CompanyIndex', 'handleWatchlistToggle', error, { stockCode, isInWatchlist });
toast({ title: '操作失败,请稍后重试', status: 'error', duration: 2000 });
} finally {
setIsWatchlistLoading(false);
}
};
return ( return (
<Container maxW="container.xl" py={5}> <Container maxW="container.xl" py={5}>
{/* 页面标题和股票搜索 */}
<VStack align="stretch" spacing={5}> <VStack align="stretch" spacing={5}>
<Card bg={bgColor} shadow="md"> {/* 页面头部:标题、搜索、自选股按钮 */}
<CardBody> <CompanyHeader
<HStack justify="space-between" align="center"> stockCode={stockCode}
<VStack align="start" spacing={1}> inputCode={inputCode}
<Heading size="lg">个股详情</Heading> onInputChange={setInputCode}
<Text color="gray.600" fontSize="sm"> onSearch={handleSearch}
查看股票实时行情财务数据和盈利预测 onKeyPress={handleKeyPress}
</Text> isInWatchlist={isInWatchlist}
</VStack> isWatchlistLoading={isWatchlistLoading}
onWatchlistToggle={handleWatchlistToggle}
bgColor={bgColor}
/>
<HStack spacing={3}> {/* Tab 切换区域:概览、行情、财务、预测 */}
<InputGroup size="lg" maxW="300px"> <CompanyTabs
<InputLeftElement pointerEvents="none"> stockCode={stockCode}
<SearchIcon color="gray.400" /> onTabChange={trackTabChanged}
</InputLeftElement> bgColor={bgColor}
<Input />
placeholder="输入股票代码"
value={inputCode}
onChange={(e) => setInputCode(e.target.value)}
onKeyPress={handleKeyPress}
borderRadius="md"
_focus={{
borderColor: 'blue.500',
boxShadow: '0 0 0 1px #3182ce'
}}
/>
</InputGroup>
<Button
colorScheme="blue"
size="lg"
onClick={handleSearch}
leftIcon={<SearchIcon />}
>
查询
</Button>
<Button
colorScheme={isInWatchlist ? 'yellow' : 'teal'}
variant={isInWatchlist ? 'solid' : 'outline'}
size="lg"
onClick={handleWatchlistToggle}
leftIcon={<StarIcon />}
isLoading={isWatchlistLoading}
>
{isInWatchlist ? '已在自选' : '加入自选'}
</Button>
<IconButton
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
onClick={toggleColorMode}
variant="outline"
colorScheme={colorMode === 'light' ? 'blue' : 'yellow'}
size="lg"
aria-label="Toggle color mode"
/>
</HStack>
</HStack>
{/* 当前股票信息 */}
<HStack mt={4} spacing={4}>
<Badge colorScheme="blue" fontSize="md" px={3} py={1}>
股票代码: {stockCode}
</Badge>
<Text fontSize="sm" color="gray.600">
更新时间: {new Date().toLocaleString()}
</Text>
</HStack>
</CardBody>
</Card>
{/* 数据展示区域 */}
<Card bg={bgColor} shadow="lg">
<CardBody p={0}>
<Tabs
variant="soft-rounded"
colorScheme="blue"
size="lg"
index={currentTabIndex}
onChange={(index) => {
const tabNames = ['公司概览', '股票行情', '财务全景', '盈利预测'];
// 🎯 追踪 Tab 切换
trackTabChanged(index, tabNames[index], currentTabIndex);
setCurrentTabIndex(index);
}}
>
<TabList p={4} bg={tabBg}>
<Tab
_selected={{
bg: activeBg,
color: 'white',
transform: 'scale(1.02)',
transition: 'all 0.2s'
}}
mr={2}
>
<HStack spacing={2}>
<Icon as={FaInfoCircle} />
<Text>公司概览</Text>
</HStack>
</Tab>
<Tab
_selected={{
bg: activeBg,
color: 'white',
transform: 'scale(1.02)',
transition: 'all 0.2s'
}}
mr={2}
>
<HStack spacing={2}>
<Icon as={FaChartLine} />
<Text>股票行情</Text>
</HStack>
</Tab>
<Tab
_selected={{
bg: activeBg,
color: 'white',
transform: 'scale(1.02)',
transition: 'all 0.2s'
}}
mr={2}
>
<HStack spacing={2}>
<Icon as={FaMoneyBillWave} />
<Text>财务全景</Text>
</HStack>
</Tab>
<Tab
_selected={{
bg: activeBg,
color: 'white',
transform: 'scale(1.02)',
transition: 'all 0.2s'
}}
>
<HStack spacing={2}>
<Icon as={FaChartBar} />
<Text>盈利预测</Text>
</HStack>
</Tab>
</TabList>
<Divider />
<TabPanels>
<TabPanel p={6}>
<CompanyOverview stockCode={stockCode} />
</TabPanel>
<TabPanel p={6}>
<MarketDataView stockCode={stockCode} />
</TabPanel>
<TabPanel p={6}>
<FinancialPanorama stockCode={stockCode} />
</TabPanel>
<TabPanel p={6}>
<ForecastReport stockCode={stockCode} />
</TabPanel>
</TabPanels>
</Tabs>
</CardBody>
</Card>
</VStack> </VStack>
</Container> </Container>
); );
}; };
export default CompanyIndex; export default CompanyIndex;