Files
vf_react/src/views/Company/index.tsx
zdl aad73a18ed fix(SubTabContainer): 移除外层 Suspense,Tab 内容直接展示
- SubTabContainer 内部为每个 Tab 添加 Suspense fallback={null}
- 移除 Company/index.tsx 外层 Suspense 和 TabLoadingFallback
- 切换一级 Tab 时不再显示整体 loading,直接展示内容

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-18 18:23:16 +08:00

266 lines
7.5 KiB
TypeScript

/**
* 公司详情页面 - FUI 科幻风格
*
* 特性:
* - Ash Thorp 风格 FUI 设计
* - James Turrell 光影效果
* - Glassmorphism 毛玻璃卡片
* - Linear.app 风格微交互
* - HeroUI 现代组件风格
*/
import React, { memo, useCallback, useRef, useEffect } from 'react';
// FUI 动画样式
import './theme/fui-animations.css';
import { useSearchParams } from 'react-router-dom';
import { Box } from '@chakra-ui/react';
import SubTabContainer from '@components/SubTabContainer';
import { useCompanyEvents } from './hooks/useCompanyEvents';
import { useCompanyData } from './hooks/useCompanyData';
import CompanyHeader from './components/CompanyHeader';
import StockQuoteCard from './components/StockQuoteCard';
import { THEME, TAB_CONFIG } from './config';
// ============================================
// 主内容区组件 - FUI 风格
// ============================================
interface CompanyContentProps {
stockCode: string;
isInWatchlist: boolean;
watchlistLoading: boolean;
onWatchlistToggle: () => void;
onTabChange: (index: number, tabKey: string) => void;
}
const CompanyContent = memo<CompanyContentProps>(({
stockCode,
isInWatchlist,
watchlistLoading,
onWatchlistToggle,
onTabChange,
}) => (
<Box maxW="container.xl" mx="auto" px={4} py={6}>
{/* 股票行情卡片 - 放在 Tab 切换器上方,始终可见 */}
<Box mb={6}>
<StockQuoteCard
stockCode={stockCode}
isInWatchlist={isInWatchlist}
isWatchlistLoading={watchlistLoading}
onWatchlistToggle={onWatchlistToggle}
/>
</Box>
{/* Tab 内容区 */}
<Box
position="relative"
bg={`linear-gradient(145deg, rgba(26, 26, 46, 0.95) 0%, rgba(15, 15, 26, 0.98) 100%)`}
borderRadius="xl"
border="1px solid"
borderColor="rgba(212, 175, 55, 0.15)"
overflow="hidden"
backdropFilter="blur(16px)"
boxShadow="0 8px 32px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.05)"
>
{/* 角落装饰 - FUI 风格 */}
<Box
position="absolute"
top="12px"
left="12px"
w="16px"
h="16px"
borderTop="2px solid"
borderLeft="2px solid"
borderColor="rgba(212, 175, 55, 0.4)"
opacity={0.6}
/>
<Box
position="absolute"
top="12px"
right="12px"
w="16px"
h="16px"
borderTop="2px solid"
borderRight="2px solid"
borderColor="rgba(212, 175, 55, 0.4)"
opacity={0.6}
/>
<Box
position="absolute"
bottom="12px"
left="12px"
w="16px"
h="16px"
borderBottom="2px solid"
borderLeft="2px solid"
borderColor="rgba(212, 175, 55, 0.4)"
opacity={0.6}
/>
<Box
position="absolute"
bottom="12px"
right="12px"
w="16px"
h="16px"
borderBottom="2px solid"
borderRight="2px solid"
borderColor="rgba(212, 175, 55, 0.4)"
opacity={0.6}
/>
<SubTabContainer
tabs={TAB_CONFIG}
componentProps={{ stockCode }}
onTabChange={onTabChange}
themePreset="blackGold"
contentPadding={0}
isLazy={true}
/>
</Box>
</Box>
));
CompanyContent.displayName = 'CompanyContent';
// ============================================
// 网页标题 Hook
// ============================================
const useDocumentTitle = (stockCode: string, stockName?: string) => {
useEffect(() => {
const baseTitle = '价值前沿';
if (stockName) {
document.title = `${stockName}(${stockCode}) - ${baseTitle}`;
} else if (stockCode) {
document.title = `${stockCode} - ${baseTitle}`;
} else {
document.title = baseTitle;
}
// 组件卸载时恢复默认标题
return () => {
document.title = baseTitle;
};
}, [stockCode, stockName]);
};
// ============================================
// 主页面组件
// ============================================
const CompanyIndex: React.FC = () => {
// URL 参数管理
const [searchParams, setSearchParams] = useSearchParams();
const stockCode = searchParams.get('scode') || '000001';
const prevStockCodeRef = useRef(stockCode);
// 数据加载 Hook
const {
stockInfo,
stockInfoLoading,
isInWatchlist,
watchlistLoading,
toggleWatchlist,
} = useCompanyData({ stockCode });
// 事件追踪 Hook
const companyEvents = useCompanyEvents({ stockCode }) as {
trackStockSearched: (newCode: string, oldCode: string | null) => void;
trackTabChanged: (index: number, name: string, prevIndex: number) => void;
trackWatchlistAdded: (code: string) => void;
trackWatchlistRemoved: (code: string) => void;
};
const { trackStockSearched, trackTabChanged, trackWatchlistAdded, trackWatchlistRemoved } = companyEvents;
// 设置网页标题
useDocumentTitle(stockCode, stockInfo?.stock_name);
// 股票代码变化追踪
useEffect(() => {
if (stockCode !== prevStockCodeRef.current) {
trackStockSearched(stockCode, prevStockCodeRef.current);
prevStockCodeRef.current = stockCode;
}
}, [stockCode, trackStockSearched]);
// 处理股票切换
const handleStockChange = useCallback((newCode: string) => {
if (newCode && newCode !== stockCode) {
trackStockSearched(newCode, stockCode);
setSearchParams({ scode: newCode });
}
}, [stockCode, setSearchParams, trackStockSearched]);
// 处理自选股切换(带追踪)
const handleWatchlistToggle = useCallback(async () => {
const wasInWatchlist = isInWatchlist;
await toggleWatchlist();
// 追踪事件(根据操作前的状态判断)
if (wasInWatchlist) {
trackWatchlistRemoved(stockCode);
} else {
trackWatchlistAdded(stockCode);
}
}, [stockCode, isInWatchlist, toggleWatchlist, trackWatchlistAdded, trackWatchlistRemoved]);
// 处理 Tab 切换
const handleTabChange = useCallback((index: number, tabKey: string) => {
const tabName = TAB_CONFIG[index]?.name || tabKey;
trackTabChanged(index, tabName, index);
}, [trackTabChanged]);
return (
<Box
position="relative"
bg={THEME.bg}
minH="calc(100vh - 60px)"
overflow="hidden"
>
{/* 全局环境光效果 - James Turrell 风格 */}
<Box
position="fixed"
top={0}
left={0}
right={0}
bottom={0}
pointerEvents="none"
zIndex={0}
bg={`
radial-gradient(ellipse 100% 80% at 50% -20%, rgba(212, 175, 55, 0.08), transparent 50%),
radial-gradient(ellipse 60% 50% at 0% 50%, rgba(100, 200, 255, 0.04), transparent 40%),
radial-gradient(ellipse 60% 50% at 100% 50%, rgba(255, 200, 100, 0.04), transparent 40%)
`}
/>
{/* 顶部搜索栏 */}
<Box position="relative" zIndex={1}>
<CompanyHeader
stockCode={stockCode}
stockInfo={stockInfo}
stockInfoLoading={stockInfoLoading}
isInWatchlist={isInWatchlist}
watchlistLoading={watchlistLoading}
onStockChange={handleStockChange}
onWatchlistToggle={handleWatchlistToggle}
/>
</Box>
{/* 主内容区 */}
<Box position="relative" zIndex={1}>
<CompanyContent
stockCode={stockCode}
isInWatchlist={isInWatchlist}
watchlistLoading={watchlistLoading}
onWatchlistToggle={handleWatchlistToggle}
onTabChange={handleTabChange}
/>
</Box>
</Box>
);
};
export default memo(CompanyIndex);