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')),
ForecastReport: React.lazy(() => import('@views/Company/ForecastReport')),
FinancialPanorama: React.lazy(() => import('@views/Company/FinancialPanorama')),
MarketDataView: React.lazy(() => import('@views/Company/MarketDataView')),
ForecastReport: React.lazy(() => import('@views/Company/components/ForecastReport')),
FinancialPanorama: React.lazy(() => import('@views/Company/components/FinancialPanorama')),
MarketDataView: React.lazy(() => import('@views/Company/components/MarketDataView')),
// Agent模块
AgentChat: React.lazy(() => import('@views/AgentChat')),

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
// src/views/Company/FinancialPanorama.jsx
import React, { useState, useEffect, useMemo } from 'react';
import { logger } from '../../utils/logger';
import { logger } from '@utils/logger';
import {
Box,
Container,
@@ -75,7 +75,7 @@ import {
ArrowDownIcon,
} from '@chakra-ui/icons';
import ReactECharts from 'echarts-for-react';
import { financialService, formatUtils, chartUtils } from '../../services/financialService';
import { financialService, formatUtils, chartUtils } from '@services/financialService';
const FinancialPanorama = ({ stockCode: propStockCode }) => {
// 状态管理
@@ -84,7 +84,7 @@ const FinancialPanorama = ({ stockCode: propStockCode }) => {
const [error, setError] = useState(null);
const [selectedPeriods, setSelectedPeriods] = useState(8);
const [activeTab, setActiveTab] = useState(0);
// 财务数据状态
const [stockInfo, setStockInfo] = useState(null);
const [balanceSheet, setBalanceSheet] = useState([]);

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 { RepeatIcon } from '@chakra-ui/icons';
import ReactECharts from 'echarts-for-react';
import { stockService } from '../../services/eventService';
import { stockService } from '@services/eventService';
const ForecastReport = ({ stockCode: propStockCode }) => {
const [code, setCode] = useState(propStockCode || '600000');

View File

@@ -1,7 +1,7 @@
// src/views/Market/MarketDataPro.jsx
import React, { useState, useEffect, useMemo } from 'react';
import { logger } from '../../utils/logger';
import { getApiBase } from '../../utils/apiConfig';
import { logger } from '@utils/logger';
import { getApiBase } from '@utils/apiConfig';
import {
Box,
Container,
@@ -303,7 +303,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState(0);
const [selectedPeriod, setSelectedPeriod] = useState(60);
// 数据状态
const [summary, setSummary] = useState(null);
const [tradeData, setTradeData] = useState([]);

View File

@@ -1,51 +1,40 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
Container,
Heading,
Card,
CardBody,
Tabs,
TabList,
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
// src/views/Company/index.js
// 公司详情页面入口 - 纯组合层
import React, { useEffect, useRef } from 'react';
import { Container, VStack, useColorModeValue } from '@chakra-ui/react';
// 自定义 Hooks
import { useCompanyStock } from './hooks/useCompanyStock';
import { useCompanyWatchlist } from './hooks/useCompanyWatchlist';
import { useCompanyEvents } from './hooks/useCompanyEvents';
const CompanyIndex = () => {
const [searchParams, setSearchParams] = useSearchParams();
const [stockCode, setStockCode] = useState(searchParams.get('scode') || '000001');
const [inputCode, setInputCode] = useState(stockCode);
const { colorMode, toggleColorMode } = useColorMode();
const toast = useToast();
const { isAuthenticated } = useAuth();
// 页面组件
import CompanyHeader from './components/CompanyHeader';
import CompanyTabs from './components/CompanyTabs';
// 🎯 PostHog 事件追踪
/**
* 公司详情页面
*
* 功能:
* - 股票搜索与代码管理
* - 自选股添加/移除
* - 多维度数据展示(概览、行情、财务、预测)
* - PostHog 事件追踪
*/
const CompanyIndex = () => {
const bgColor = useColorModeValue('white', 'gray.800');
// 1. 先获取股票代码(不带追踪回调)
const {
stockCode,
inputCode,
setInputCode,
handleSearch,
handleKeyPress,
} = useCompanyStock();
// 2. 再初始化事件追踪(传入 stockCode
const {
trackStockSearched,
trackTabChanged,
@@ -53,297 +42,53 @@ const CompanyIndex = () => {
trackWatchlistRemoved,
} = useCompanyEvents({ stockCode });
// Tab 索引状态(用于追踪 Tab 切换)
const [currentTabIndex, setCurrentTabIndex] = useState(0);
// 3. 自选股管理
const {
isInWatchlist,
isLoading: isWatchlistLoading,
toggle: handleWatchlistToggle,
} = useCompanyWatchlist({
stockCode,
tracking: {
onAdd: trackWatchlistAdded,
onRemove: trackWatchlistRemoved,
},
});
const bgColor = useColorModeValue('white', 'gray.800');
const tabBg = useColorModeValue('gray.50', 'gray.700');
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参数变化时更新股票代码
// 4. 监听 stockCode 变化,触发搜索追踪
const prevStockCodeRef = useRef(stockCode);
useEffect(() => {
const scode = searchParams.get('scode');
if (scode && scode !== stockCode) {
setStockCode(scode);
setInputCode(scode);
if (stockCode !== prevStockCodeRef.current) {
trackStockSearched(stockCode, prevStockCodeRef.current);
prevStockCodeRef.current = stockCode;
}
}, [searchParams, stockCode]);
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);
}
};
}, [stockCode, trackStockSearched]);
return (
<Container maxW="container.xl" py={5}>
{/* 页面标题和股票搜索 */}
<VStack align="stretch" spacing={5}>
<Card bg={bgColor} shadow="md">
<CardBody>
<HStack justify="space-between" align="center">
<VStack align="start" spacing={1}>
<Heading size="lg">个股详情</Heading>
<Text color="gray.600" fontSize="sm">
查看股票实时行情财务数据和盈利预测
</Text>
</VStack>
<HStack spacing={3}>
<InputGroup size="lg" maxW="300px">
<InputLeftElement pointerEvents="none">
<SearchIcon color="gray.400" />
</InputLeftElement>
<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>
{/* 页面头部:标题、搜索、自选股按钮 */}
<CompanyHeader
stockCode={stockCode}
inputCode={inputCode}
onInputChange={setInputCode}
onSearch={handleSearch}
onKeyPress={handleKeyPress}
isInWatchlist={isInWatchlist}
isWatchlistLoading={isWatchlistLoading}
onWatchlistToggle={handleWatchlistToggle}
bgColor={bgColor}
/>
{/* Tab 切换区域:概览、行情、财务、预测 */}
<CompanyTabs
stockCode={stockCode}
onTabChange={trackTabChanged}
bgColor={bgColor}
/>
</VStack>
</Container>
);
};
export default CompanyIndex;