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:
@@ -1,147 +0,0 @@
|
||||
// src/views/Company/components/CompanyHeader/SearchBar.js
|
||||
// 股票搜索栏组件 - 金色主题 + 模糊搜索下拉
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
Text,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { SearchIcon } from '@chakra-ui/icons';
|
||||
import { useStockSearch } from '../../hooks/useStockSearch';
|
||||
|
||||
/**
|
||||
* 股票搜索栏组件(带模糊搜索下拉)
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {string} props.inputCode - 输入框当前值
|
||||
* @param {Function} props.onInputChange - 输入变化回调
|
||||
* @param {Function} props.onSearch - 搜索按钮点击回调
|
||||
* @param {Function} props.onKeyDown - 键盘事件回调
|
||||
*/
|
||||
const SearchBar = ({
|
||||
inputCode,
|
||||
onInputChange,
|
||||
onSearch,
|
||||
onKeyDown,
|
||||
}) => {
|
||||
// 下拉状态
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const containerRef = useRef(null);
|
||||
|
||||
// 从 Redux 获取全部股票列表
|
||||
const allStocks = useSelector(state => state.stock.allStocks);
|
||||
|
||||
// 使用共享的搜索 Hook
|
||||
const filteredStocks = useStockSearch(allStocks, inputCode, { limit: 10 });
|
||||
|
||||
// 根据搜索结果更新下拉显示状态
|
||||
useEffect(() => {
|
||||
setShowDropdown(filteredStocks.length > 0 && !!inputCode?.trim());
|
||||
}, [filteredStocks, inputCode]);
|
||||
|
||||
// 点击外部关闭下拉
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
||||
setShowDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// 选择股票 - 直接触发搜索跳转
|
||||
const handleSelectStock = (stock) => {
|
||||
onInputChange(stock.code);
|
||||
setShowDropdown(false);
|
||||
onSearch(stock.code);
|
||||
};
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeyDownWrapper = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setShowDropdown(false);
|
||||
}
|
||||
onKeyDown?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box ref={containerRef} position="relative" w="300px">
|
||||
<InputGroup size="lg">
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<SearchIcon color="#C9A961" />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
placeholder="输入股票代码或名称"
|
||||
value={inputCode}
|
||||
onChange={(e) => onInputChange(e.target.value)}
|
||||
onKeyDown={handleKeyDownWrapper}
|
||||
onFocus={() => inputCode && filteredStocks.length > 0 && setShowDropdown(true)}
|
||||
borderRadius="md"
|
||||
color="white"
|
||||
borderColor="#C9A961"
|
||||
_placeholder={{ color: '#C9A961' }}
|
||||
_focus={{
|
||||
borderColor: '#F4D03F',
|
||||
boxShadow: '0 0 0 1px #F4D03F',
|
||||
}}
|
||||
_hover={{
|
||||
borderColor: '#F4D03F',
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
{/* 模糊搜索下拉列表 */}
|
||||
{showDropdown && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top="100%"
|
||||
left={0}
|
||||
mt={1}
|
||||
w="100%"
|
||||
bg="#1A202C"
|
||||
border="1px solid #C9A961"
|
||||
borderRadius="md"
|
||||
maxH="300px"
|
||||
overflowY="auto"
|
||||
zIndex={1000}
|
||||
boxShadow="0 4px 12px rgba(0, 0, 0, 0.3)"
|
||||
>
|
||||
<VStack align="stretch" spacing={0}>
|
||||
{filteredStocks.map((stock) => (
|
||||
<Box
|
||||
key={stock.code}
|
||||
px={4}
|
||||
py={2}
|
||||
cursor="pointer"
|
||||
_hover={{ bg: 'whiteAlpha.100' }}
|
||||
onClick={() => handleSelectStock(stock)}
|
||||
borderBottom="1px solid"
|
||||
borderColor="whiteAlpha.100"
|
||||
_last={{ borderBottom: 'none' }}
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<Text color="#F4D03F" fontWeight="bold" fontSize="sm">
|
||||
{stock.code}
|
||||
</Text>
|
||||
<Text color="#C9A961" fontSize="sm" noOfLines={1} maxW="180px">
|
||||
{stock.name}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBar;
|
||||
@@ -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>
|
||||
|
||||
@@ -74,45 +74,46 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
|
||||
const [basicLoading, setBasicLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 用于手动刷新的 ref
|
||||
// 用于手动刷新的 ref(并行请求)
|
||||
const refetchRef = useCallback(async () => {
|
||||
if (!stockCode) return;
|
||||
|
||||
// 标准化股票代码(去除后缀)
|
||||
const baseCode = stockCode.split('.')[0];
|
||||
|
||||
// 获取行情详情数据(使用新的 quote-detail 接口)
|
||||
// 并行获取行情详情和基本信息
|
||||
setQuoteLoading(true);
|
||||
setBasicLoading(true);
|
||||
setError(null);
|
||||
|
||||
logger.debug('useStockQuoteData', '刷新股票数据', { stockCode, baseCode });
|
||||
|
||||
try {
|
||||
logger.debug('useStockQuoteData', '获取股票行情详情', { stockCode, baseCode });
|
||||
const { data: result } = await axios.get(`/api/stock/${baseCode}/quote-detail`);
|
||||
if (result.success && result.data) {
|
||||
const transformedData = transformQuoteData(result.data, stockCode);
|
||||
const [quoteResult, basicResult] = await Promise.all([
|
||||
axios.get(`/api/stock/${baseCode}/quote-detail`),
|
||||
axios.get(`/api/stock/${baseCode}/basic-info`),
|
||||
]);
|
||||
|
||||
// 处理行情数据
|
||||
if (quoteResult.data.success && quoteResult.data.data) {
|
||||
const transformedData = transformQuoteData(quoteResult.data.data, stockCode);
|
||||
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
|
||||
setQuoteData(transformedData);
|
||||
} else {
|
||||
setError('获取行情数据失败');
|
||||
setQuoteData(null);
|
||||
}
|
||||
|
||||
// 处理基本信息
|
||||
if (basicResult.data.success) {
|
||||
setBasicInfo(basicResult.data.data);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('useStockQuoteData', '获取行情失败', err);
|
||||
setError('获取行情数据失败');
|
||||
logger.error('useStockQuoteData', '刷新数据失败', err);
|
||||
setError('刷新数据失败');
|
||||
setQuoteData(null);
|
||||
} finally {
|
||||
setQuoteLoading(false);
|
||||
}
|
||||
|
||||
// 获取基本信息(公司简介等)
|
||||
setBasicLoading(true);
|
||||
try {
|
||||
const { data: result } = await axios.get(`/api/stock/${baseCode}/basic-info`);
|
||||
if (result.success) {
|
||||
setBasicInfo(result.data);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('useStockQuoteData', '获取基本信息失败', err);
|
||||
} finally {
|
||||
setBasicLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
@@ -132,47 +133,45 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
|
||||
const baseCode = stockCode.split('.')[0];
|
||||
|
||||
const fetchData = async () => {
|
||||
// 获取行情详情数据(使用新的 quote-detail 接口)
|
||||
// 并行获取行情详情和基本信息(优化:原串行改为并行,节省 ~120ms)
|
||||
setQuoteLoading(true);
|
||||
setBasicLoading(true);
|
||||
setError(null);
|
||||
|
||||
logger.debug('useStockQuoteData', '并行获取股票数据', { stockCode, baseCode });
|
||||
|
||||
try {
|
||||
logger.debug('useStockQuoteData', '获取股票行情详情', { stockCode, baseCode });
|
||||
const { data: result } = await axios.get(`/api/stock/${baseCode}/quote-detail`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
const [quoteResult, basicResult] = await Promise.all([
|
||||
axios.get(`/api/stock/${baseCode}/quote-detail`, { signal: controller.signal }),
|
||||
axios.get(`/api/stock/${baseCode}/basic-info`, { signal: controller.signal }),
|
||||
]);
|
||||
|
||||
if (isCancelled) return;
|
||||
if (result.success && result.data) {
|
||||
const transformedData = transformQuoteData(result.data, stockCode);
|
||||
|
||||
// 处理行情数据
|
||||
if (quoteResult.data.success && quoteResult.data.data) {
|
||||
const transformedData = transformQuoteData(quoteResult.data.data, stockCode);
|
||||
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
|
||||
setQuoteData(transformedData);
|
||||
} else {
|
||||
setError('获取行情数据失败');
|
||||
setQuoteData(null);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (isCancelled || err.name === 'CanceledError') return;
|
||||
logger.error('useStockQuoteData', '获取行情失败', err);
|
||||
setError('获取行情数据失败');
|
||||
setQuoteData(null);
|
||||
} finally {
|
||||
if (!isCancelled) setQuoteLoading(false);
|
||||
}
|
||||
|
||||
// 获取基本信息(公司简介等)
|
||||
setBasicLoading(true);
|
||||
try {
|
||||
const { data: result } = await axios.get(`/api/stock/${baseCode}/basic-info`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (isCancelled) return;
|
||||
if (result.success) {
|
||||
setBasicInfo(result.data);
|
||||
// 处理基本信息
|
||||
if (basicResult.data.success) {
|
||||
setBasicInfo(basicResult.data.data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (isCancelled || err.name === 'CanceledError') return;
|
||||
logger.error('useStockQuoteData', '获取基本信息失败', err);
|
||||
logger.error('useStockQuoteData', '获取数据失败', err);
|
||||
setError('获取数据失败');
|
||||
setQuoteData(null);
|
||||
} finally {
|
||||
if (!isCancelled) setBasicLoading(false);
|
||||
if (!isCancelled) {
|
||||
setQuoteLoading(false);
|
||||
setBasicLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user