- 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>
266 lines
7.5 KiB
TypeScript
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);
|