refactor(Company): 简化 CompanyHeader,添加详细代码注释

- CompanyHeader: 移除冗余的股票信息展示(已在 StockQuoteCard 中)
- index.tsx: 添加完整的 JSDoc 注释和架构说明
- types.ts: 简化 CompanyHeaderProps,移除不再需要的属性
- useStockQuoteData: 优化数据获取逻辑

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-19 10:58:49 +08:00
parent 986ec05eb1
commit 1730a59ca2
5 changed files with 493 additions and 564 deletions

View File

@@ -2,10 +2,9 @@
* Company 页面顶部搜索栏组件 - FUI 科幻风格
*
* 设计特点:
* - Glassmorphism 毛玻璃背景
* - 发光效果和微动画
* - Ash Thorp 风格的数据展示
* - James Turrell 柔和光影
* - 左侧固定标题 + 副标题
* - 右侧简洁搜索框
* - 深色背景 + 金色强调色
*/
import React, { memo, useMemo, useCallback, useState } from 'react';
@@ -15,150 +14,52 @@ import {
HStack,
VStack,
Text,
Button,
Icon,
Skeleton,
} from '@chakra-ui/react';
import { AutoComplete, Spin } from 'antd';
import { Search, Star, TrendingUp, TrendingDown } from 'lucide-react';
import { AutoComplete, Input, Spin } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import { useStockSearch } from '@hooks/useStockSearch';
import { THEME, getSearchBoxStyles } from '../../config';
import { THEME } from '../../config';
import { FUI_COLORS, FUI_GLOW, FUI_ANIMATION, FUI_GLASS } from '../../theme/fui';
import type { CompanyHeaderProps, StockSearchResult } from '../../types';
/**
* 股票信息展示组件 - FUI 风格
* 页面标题组件
*/
const StockInfoDisplay = memo<{
stockCode: string;
stockName?: string;
price?: number | null;
change?: number | null;
loading: boolean;
}>(({ stockCode, stockName, price, change, loading }) => {
if (loading) {
return (
<VStack align="start" spacing={2}>
<Skeleton height="36px" width="220px" startColor={FUI_COLORS.bg.surface} endColor={FUI_COLORS.bg.elevated} />
<Skeleton height="28px" width="160px" startColor={FUI_COLORS.bg.surface} endColor={FUI_COLORS.bg.elevated} />
</VStack>
);
}
const PageTitle = memo(() => (
<VStack align="start" spacing={1}>
<Text
fontSize="2xl"
fontWeight="bold"
color={FUI_COLORS.gold[400]}
letterSpacing="wider"
textShadow={FUI_GLOW.text.gold}
>
</Text>
<Text
fontSize="sm"
color={FUI_COLORS.text.muted}
letterSpacing="wide"
>
</Text>
</VStack>
));
const isPositive = change !== null && change !== undefined && change >= 0;
const TrendIcon = isPositive ? TrendingUp : TrendingDown;
return (
<VStack align="start" spacing={1}>
{/* 股票代码 & 名称 */}
<HStack spacing={3} align="baseline">
<Text
fontSize="2xl"
fontWeight="bold"
color={FUI_COLORS.gold[400]}
letterSpacing="wider"
textShadow={FUI_GLOW.text.gold}
>
{stockCode}
</Text>
{stockName && (
<Text
fontSize="xl"
fontWeight="medium"
color={FUI_COLORS.text.primary}
letterSpacing="wide"
>
{stockName}
</Text>
)}
</HStack>
{/* 价格 & 涨跌幅 */}
{price !== null && price !== undefined && (
<HStack spacing={4} mt={1}>
{/* 价格 */}
<HStack spacing={1}>
<Text
fontSize="xs"
color={FUI_COLORS.text.muted}
textTransform="uppercase"
letterSpacing="wider"
>
Price
</Text>
<Text
fontSize="xl"
fontWeight="bold"
color={FUI_COLORS.text.primary}
fontFamily="mono"
>
¥{price.toFixed(2)}
</Text>
</HStack>
{/* 涨跌幅 Badge */}
{change !== null && change !== undefined && (
<Box
display="inline-flex"
alignItems="center"
gap={1}
px={3}
py={1}
borderRadius="full"
bg={isPositive ? 'rgba(239, 68, 68, 0.15)' : 'rgba(34, 197, 94, 0.15)'}
border="1px solid"
borderColor={isPositive ? 'rgba(239, 68, 68, 0.3)' : 'rgba(34, 197, 94, 0.3)'}
boxShadow={isPositive
? '0 0 12px rgba(239, 68, 68, 0.2)'
: '0 0 12px rgba(34, 197, 94, 0.2)'
}
>
<Icon
as={TrendIcon}
boxSize={3.5}
color={isPositive ? FUI_COLORS.status.positive : FUI_COLORS.status.negative}
/>
<Text
fontSize="sm"
fontWeight="bold"
fontFamily="mono"
color={isPositive ? FUI_COLORS.status.positive : FUI_COLORS.status.negative}
>
{isPositive ? '+' : ''}{change.toFixed(2)}%
</Text>
</Box>
)}
</HStack>
)}
</VStack>
);
});
StockInfoDisplay.displayName = 'StockInfoDisplay';
PageTitle.displayName = 'PageTitle';
/**
* 搜索操作区组件(状态自管理,减少父组件重渲染)
* 搜索组件(状态自管理,减少父组件重渲染)
*/
const SearchActions = memo<{
const SearchBox = memo<{
stockCode: string;
onStockChange: (value: string) => void;
isInWatchlist: boolean;
watchlistLoading: boolean;
onWatchlistToggle: () => void;
}>(({
stockCode,
onStockChange,
isInWatchlist,
watchlistLoading,
onWatchlistToggle,
}) => {
// 输入状态自管理(避免父组件重渲染)
const [inputCode, setInputCode] = useState(stockCode);
// 同步外部 stockCode 变化
React.useEffect(() => {
setInputCode(stockCode);
}, [stockCode]);
// 输入状态 - 默认为空,显示 placeholder
const [inputCode, setInputCode] = useState('');
// 股票搜索 Hook
const searchHook = useStockSearch({
@@ -194,13 +95,6 @@ const SearchActions = memo<{
}));
}, [searchResults]);
// 处理搜索按钮点击
const handleSearch = useCallback(() => {
if (inputCode && inputCode !== stockCode) {
onStockChange(inputCode);
}
}, [inputCode, stockCode, onStockChange]);
// 选中股票
const handleSelect = useCallback((value: string) => {
clearSearch();
@@ -210,167 +104,101 @@ const SearchActions = memo<{
}
}, [clearSearch, stockCode, onStockChange]);
// 键盘事件
// 键盘事件 - 回车搜索
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearch();
if (e.key === 'Enter' && inputCode && inputCode !== stockCode) {
onStockChange(inputCode);
}
}, [handleSearch]);
}, [inputCode, stockCode, onStockChange]);
return (
<HStack spacing={3}>
{/* 搜索框 - FUI 风格 */}
<Box
sx={{
...getSearchBoxStyles(THEME),
'.ant-select-selector': {
backgroundColor: `${FUI_COLORS.bg.primary} !important`,
borderColor: `${FUI_COLORS.line.default} !important`,
borderRadius: '10px !important',
height: '42px !important',
backdropFilter: FUI_GLASS.blur.sm,
transition: `all ${FUI_ANIMATION.duration.fast} ${FUI_ANIMATION.easing.default}`,
'&:hover': {
borderColor: `${FUI_COLORS.line.emphasis} !important`,
boxShadow: FUI_GLOW.gold.sm,
},
'&:focus-within': {
borderColor: `${FUI_COLORS.gold[400]} !important`,
boxShadow: FUI_GLOW.gold.md,
},
<Box
sx={{
'.ant-select': {
width: '320px !important',
},
'.ant-input-affix-wrapper': {
backgroundColor: 'transparent !important',
borderColor: `${FUI_COLORS.gold[400]} !important`,
borderWidth: '1px !important',
borderRadius: '6px !important',
height: '44px !important',
padding: '0 12px !important',
transition: `all ${FUI_ANIMATION.duration.fast} ${FUI_ANIMATION.easing.default}`,
'&:hover': {
borderColor: `${FUI_COLORS.gold[300]} !important`,
boxShadow: FUI_GLOW.gold.sm,
},
'.ant-select-selection-search-input': {
color: `${FUI_COLORS.text.primary} !important`,
height: '40px !important',
fontFamily: 'inherit',
'&:focus-within, &.ant-input-affix-wrapper-focused': {
borderColor: `${FUI_COLORS.gold[300]} !important`,
boxShadow: `${FUI_GLOW.gold.md} !important`,
},
'.ant-select-selection-placeholder': {
color: `${FUI_COLORS.text.muted} !important`,
},
'.ant-input': {
backgroundColor: 'transparent !important',
color: `${FUI_COLORS.gold[400]} !important`,
fontSize: '14px !important',
'&::placeholder': {
color: `${FUI_COLORS.gold[400]} !important`,
opacity: '0.7 !important',
},
},
'.ant-input-prefix': {
marginRight: '8px !important',
},
}}
>
<AutoComplete
popupClassName="fui-autocomplete-dropdown"
value={inputCode}
options={stockOptions}
onSearch={doSearch}
onSelect={handleSelect}
onChange={setInputCode}
style={{ width: 320 }}
dropdownStyle={{
backgroundColor: FUI_COLORS.bg.elevated,
borderRadius: '6px',
border: `1px solid ${FUI_COLORS.gold[400]}`,
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.5)',
}}
notFoundContent={isSearching ? <Spin size="small" /> : null}
>
<AutoComplete
popupClassName="fui-autocomplete-dropdown"
value={inputCode}
options={stockOptions}
onSearch={doSearch}
onSelect={handleSelect}
onChange={setInputCode}
placeholder="输入代码、名称或拼音"
style={{ width: 240 }}
dropdownStyle={{
backgroundColor: FUI_COLORS.bg.elevated,
borderRadius: '10px',
border: `1px solid ${FUI_COLORS.line.emphasis}`,
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.5)',
}}
notFoundContent={isSearching ? <Spin size="small" /> : null}
<Input
placeholder="输入股票代码或名称"
prefix={<SearchOutlined style={{ color: FUI_COLORS.gold[400], fontSize: 16 }} />}
onKeyDown={handleKeyDown}
style={{
backgroundColor: 'transparent',
borderColor: FUI_COLORS.gold[400],
borderRadius: 6,
height: 44,
color: FUI_COLORS.gold[400],
}}
/>
</Box>
{/* 搜索按钮 - 发光效果 */}
<Button
bg={`linear-gradient(135deg, ${FUI_COLORS.gold[500]} 0%, ${FUI_COLORS.gold[400]} 100%)`}
color={FUI_COLORS.bg.deep}
_hover={{
bg: `linear-gradient(135deg, ${FUI_COLORS.gold[400]} 0%, ${FUI_COLORS.gold[300]} 100%)`,
boxShadow: FUI_GLOW.gold.md,
transform: 'translateY(-1px)',
}}
_active={{
bg: FUI_COLORS.gold[600],
transform: 'translateY(0)',
}}
size="md"
h="42px"
px={5}
onClick={handleSearch}
leftIcon={<Icon as={Search} boxSize={4} />}
fontWeight="bold"
borderRadius="10px"
transition={`all ${FUI_ANIMATION.duration.fast} ${FUI_ANIMATION.easing.default}`}
>
</Button>
{/* 自选按钮 - FUI 风格 */}
<Button
variant="outline"
bg={isInWatchlist ? 'rgba(212, 175, 55, 0.2)' : 'transparent'}
color={FUI_COLORS.gold[400]}
borderColor={isInWatchlist ? FUI_COLORS.gold[400] : FUI_COLORS.line.emphasis}
borderWidth="1px"
_hover={{
bg: 'rgba(212, 175, 55, 0.15)',
borderColor: FUI_COLORS.gold[400],
boxShadow: FUI_GLOW.gold.sm,
transform: 'translateY(-1px)',
}}
_active={{
bg: 'rgba(212, 175, 55, 0.25)',
transform: 'translateY(0)',
}}
size="md"
h="42px"
px={5}
onClick={onWatchlistToggle}
isLoading={watchlistLoading}
leftIcon={
<Icon
as={Star}
boxSize={4}
fill={isInWatchlist ? 'currentColor' : 'none'}
filter={isInWatchlist ? 'drop-shadow(0 0 4px rgba(212, 175, 55, 0.6))' : 'none'}
/>
}
fontWeight="bold"
borderRadius="10px"
transition={`all ${FUI_ANIMATION.duration.fast} ${FUI_ANIMATION.easing.default}`}
sx={isInWatchlist ? { animation: 'glowPulse 3s ease-in-out infinite' } : undefined}
>
{isInWatchlist ? '已自选' : '自选'}
</Button>
</HStack>
</AutoComplete>
</Box>
);
});
SearchActions.displayName = 'SearchActions';
SearchBox.displayName = 'SearchBox';
/**
* Company 页面顶部组件
*/
const CompanyHeader: React.FC<CompanyHeaderProps> = memo(({
stockCode,
stockInfo,
stockInfoLoading,
isInWatchlist,
watchlistLoading,
onStockChange,
onWatchlistToggle,
}) => {
return (
<Box
position="relative"
bg={`linear-gradient(180deg, ${FUI_COLORS.bg.elevated} 0%, ${FUI_COLORS.bg.primary} 100%)`}
bg={FUI_COLORS.bg.primary}
borderBottom="1px solid"
borderColor={FUI_COLORS.line.default}
px={6}
py={5}
backdropFilter={FUI_GLASS.blur.md}
overflow="hidden"
py={4}
>
{/* 顶部发光线(环境光效果由全局 AmbientGlow 提供) */}
<Box
position="absolute"
top={0}
left="10%"
right="10%"
h="1px"
bg={`linear-gradient(90deg, transparent 0%, ${FUI_COLORS.gold[400]} 50%, transparent 100%)`}
opacity={0.4}
/>
<Flex
position="relative"
zIndex={1}
@@ -378,25 +206,14 @@ const CompanyHeader: React.FC<CompanyHeaderProps> = memo(({
mx="auto"
justify="space-between"
align="center"
wrap="wrap"
gap={4}
>
{/* 左侧:股票信息 */}
<StockInfoDisplay
stockCode={stockCode}
stockName={stockInfo?.stock_name}
price={stockInfo?.close_price}
change={stockInfo?.change_pct}
loading={stockInfoLoading}
/>
{/* 左侧:页面标题 */}
<PageTitle />
{/* 右侧:搜索和操作 */}
<SearchActions
{/* 右侧:搜索 */}
<SearchBox
stockCode={stockCode}
onStockChange={onStockChange}
isInWatchlist={isInWatchlist}
watchlistLoading={watchlistLoading}
onWatchlistToggle={onWatchlistToggle}
/>
</Flex>
</Box>