更新ios

This commit is contained in:
2026-01-23 15:59:33 +08:00
parent b9177fcbf0
commit c7f340f0ed
4 changed files with 381 additions and 86 deletions

View File

@@ -0,0 +1,213 @@
// src/components/Navbars/components/NavStockSearch/index.js
// 导航栏股票搜索组件 - 支持代码、名称、拼音缩写搜索
import React, { useRef, useEffect, useCallback, memo } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Box,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
IconButton,
Text,
VStack,
HStack,
Spinner,
Tag,
Center,
List,
ListItem,
Flex,
Portal,
} from '@chakra-ui/react';
import { Search, X } from 'lucide-react';
import { useStockSearch } from '@hooks/useStockSearch';
/**
* 导航栏股票搜索组件
* 浅色主题,适配导航栏使用
*/
const NavStockSearch = memo(() => {
const navigate = useNavigate();
const containerRef = useRef(null);
const inputRef = useRef(null);
// 浅色主题颜色配置
const searchIconColor = 'gray.400';
const inputBg = 'gray.50';
const dropdownBg = 'white';
const borderColor = 'gray.200';
const hoverBg = 'gray.50';
const textColor = 'gray.800';
const subTextColor = 'gray.500';
const accentColor = 'blue.500';
// 使用搜索 Hook
const {
searchQuery,
searchResults,
isSearching,
showResults,
handleSearch,
clearSearch,
setShowResults,
} = useStockSearch({ limit: 8, debounceMs: 300 });
// 点击外部关闭下拉
useEffect(() => {
const handleClickOutside = (event) => {
if (containerRef.current && !containerRef.current.contains(event.target)) {
setShowResults(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [setShowResults]);
// 选择股票 - 跳转到详情页
const handleSelectStock = useCallback((stock) => {
clearSearch();
navigate(`/company/${stock.stock_code}`);
}, [navigate, clearSearch]);
// 处理键盘事件
const handleKeyDown = useCallback((e) => {
if (e.key === 'Enter' && searchResults.length > 0) {
handleSelectStock(searchResults[0]);
} else if (e.key === 'Escape') {
setShowResults(false);
inputRef.current?.blur();
}
}, [searchResults, handleSelectStock, setShowResults]);
// 计算下拉框位置
const getDropdownPosition = () => {
if (!containerRef.current) return {};
const rect = containerRef.current.getBoundingClientRect();
return {
position: 'fixed',
top: `${rect.bottom + 8}px`,
left: `${rect.left}px`,
width: '300px',
};
};
return (
<Box ref={containerRef} position="relative">
<InputGroup size="sm" w="180px">
<InputLeftElement pointerEvents="none">
<Search color="#A0AEC0" size={14} />
</InputLeftElement>
<Input
ref={inputRef}
fontSize="sm"
bg={inputBg}
placeholder="搜索股票"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
onKeyDown={handleKeyDown}
onFocus={() => searchQuery && searchResults.length > 0 && setShowResults(true)}
borderColor={borderColor}
borderRadius="md"
_hover={{ borderColor: 'gray.300' }}
_focus={{
borderColor: accentColor,
boxShadow: '0 0 0 1px var(--chakra-colors-blue-500)',
bg: 'white'
}}
_placeholder={{ color: 'gray.400' }}
/>
{(searchQuery || isSearching) && (
<InputRightElement>
{isSearching ? (
<Spinner size="xs" color={accentColor} />
) : (
<IconButton
size="xs"
variant="ghost"
icon={<X size={12} />}
onClick={clearSearch}
aria-label="清除搜索"
color="gray.400"
_hover={{ bg: 'transparent', color: 'gray.600' }}
/>
)}
</InputRightElement>
)}
</InputGroup>
{/* 搜索结果下拉 - 使用 Portal 避免被导航栏裁剪 */}
{showResults && (
<Portal>
<Box
{...getDropdownPosition()}
bg={dropdownBg}
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
boxShadow="lg"
maxH="360px"
overflowY="auto"
zIndex={9999}
>
{searchResults.length > 0 ? (
<List spacing={0}>
{searchResults.map((stock, index) => (
<ListItem
key={stock.stock_code}
px={3}
py={2.5}
cursor="pointer"
_hover={{ bg: hoverBg }}
onClick={() => handleSelectStock(stock)}
borderBottomWidth={index < searchResults.length - 1 ? '1px' : '0'}
borderColor="gray.100"
>
<Flex align="center" justify="space-between">
<VStack align="start" spacing={0} flex={1}>
<Text fontWeight="medium" color={textColor} fontSize="sm">
{stock.stock_name}
</Text>
<HStack spacing={2}>
<Text fontSize="xs" color={subTextColor}>
{stock.stock_code}
</Text>
{stock.pinyin_abbr && (
<Text fontSize="xs" color="gray.400">
{stock.pinyin_abbr.toUpperCase()}
</Text>
)}
</HStack>
</VStack>
{stock.exchange && (
<Tag
size="sm"
colorScheme={stock.exchange === 'SH' ? 'red' : 'blue'}
variant="subtle"
fontSize="xs"
>
{stock.exchange === 'SH' ? '沪' : stock.exchange === 'SZ' ? '深' : stock.exchange}
</Tag>
)}
</Flex>
</ListItem>
))}
</List>
) : (
<Center p={4}>
<Text color={subTextColor} fontSize="sm">
{searchQuery ? '未找到相关股票' : '输入代码/名称/拼音搜索'}
</Text>
</Center>
)}
</Box>
</Portal>
)}
</Box>
);
});
NavStockSearch.displayName = 'NavStockSearch';
export default NavStockSearch;

View File

@@ -8,6 +8,7 @@ import LoginButton from '../LoginButton';
// import CalendarButton from '../CalendarButton'; // 暂时注释 // import CalendarButton from '../CalendarButton'; // 暂时注释
import { DesktopUserMenu, TabletUserMenu } from '../UserMenu'; import { DesktopUserMenu, TabletUserMenu } from '../UserMenu';
import { MySpaceButton, MoreMenu } from '../Navigation'; import { MySpaceButton, MoreMenu } from '../Navigation';
import NavStockSearch from '../NavStockSearch';
/** /**
* Navbar 右侧功能区组件 * Navbar 右侧功能区组件
@@ -50,9 +51,10 @@ const NavbarActions = memo(({
{/* 投资日历 - 暂时注释 */} {/* 投资日历 - 暂时注释 */}
{/* {isDesktop && <CalendarButton />} */} {/* {isDesktop && <CalendarButton />} */}
{/* 桌面端布局:[我的空间] | [头像][用户名] */} {/* 桌面端布局:[搜索框] [我的空间] | [头像][用户名] */}
{isDesktop ? ( {isDesktop ? (
<> <>
<NavStockSearch />
<MySpaceButton /> <MySpaceButton />
<Divider <Divider
orientation="vertical" orientation="vertical"

View File

@@ -153,13 +153,16 @@ const Community = () => {
{/* 主内容区域 - padding 由 MainLayout 统一设置 */} {/* 主内容区域 - padding 由 MainLayout 统一设置 */}
<Box ref={containerRef} pt={0} pb={0}> <Box ref={containerRef} pt={0} pb={0}>
{/* ⚡ 顶部说明面板(懒加载):产品介绍 + 沪深指数 + 热门概念词云 */} {/* ⚡ 顶部说明面板(懒加载):产品介绍 + 沪深指数 + 热门概念词云 */}
<Suspense fallback={ {/* 📱 移动端隐藏HeroPanel 在小屏幕上显示效果不佳,仅在 md 及以上尺寸显示 */}
<Box mb={6} p={4} borderRadius="xl" bg="rgba(255,255,255,0.02)"> <Box display={{ base: 'none', md: 'block' }}>
<Skeleton height="200px" borderRadius="lg" startColor="gray.800" endColor="gray.700" /> <Suspense fallback={
</Box> <Box mb={6} p={4} borderRadius="xl" bg="rgba(255,255,255,0.02)">
}> <Skeleton height="200px" borderRadius="lg" startColor="gray.800" endColor="gray.700" />
<HeroPanel /> </Box>
</Suspense> }>
<HeroPanel />
</Suspense>
</Box>
{/* 实时要闻·动态追踪 - 横向滚动 */} {/* 实时要闻·动态追踪 - 横向滚动 */}
<DynamicNewsCard <DynamicNewsCard

View File

@@ -1,8 +1,21 @@
// 关注股票面板 - 紧凑版 // 关注股票面板 - 紧凑版
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Box, Text, VStack, HStack, Icon, Badge } from '@chakra-ui/react'; import {
Box,
Text,
VStack,
HStack,
Icon,
Badge,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
Portal,
} from '@chakra-ui/react';
import { BarChart2, Plus } from 'lucide-react'; import { BarChart2, Plus } from 'lucide-react';
import FavoriteButton from '@/components/FavoriteButton'; import FavoriteButton from '@/components/FavoriteButton';
import { MiniTimelineChart } from '@components/Charts/Stock';
/** /**
* 格式化涨跌幅 * 格式化涨跌幅
@@ -104,87 +117,151 @@ const WatchlistPanel = ({
const weeklyChg = formatChange(quote?.weekly_chg ?? stock.weekly_chg); const weeklyChg = formatChange(quote?.weekly_chg ?? stock.weekly_chg);
return ( return (
<Box <Popover
key={stock.stock_code} key={stock.stock_code}
py={2} trigger="hover"
px={2} placement="left"
cursor="pointer" openDelay={300}
borderRadius="md" closeDelay={100}
bg="rgba(37, 37, 64, 0.3)" isLazy
_hover={{ bg: 'rgba(37, 37, 64, 0.6)' }}
onClick={() => onStockClick?.(stock)}
role="group"
> >
{/* 第一行:股票名称 + 价格/涨跌幅 + 取消关注按钮 */} <PopoverTrigger>
<HStack justify="space-between" mb={1.5}> <Box
<VStack align="start" spacing={0} flex={1} minW={0}> py={2}
<Text px={2}
fontSize="xs" cursor="pointer"
fontWeight="medium" borderRadius="md"
color="rgba(255, 255, 255, 0.9)" bg="rgba(37, 37, 64, 0.3)"
noOfLines={1} _hover={{ bg: 'rgba(37, 37, 64, 0.6)' }}
> onClick={() => onStockClick?.(stock)}
{stock.stock_name || stock.stock_code} role="group"
</Text> >
<Text fontSize="10px" color="rgba(255, 255, 255, 0.4)"> {/* 第一行:股票名称 + 价格/涨跌幅 + 取消关注按钮 */}
{stock.stock_code} <HStack justify="space-between" mb={1.5}>
</Text> <VStack align="start" spacing={0} flex={1} minW={0}>
</VStack> <Text
<HStack spacing={1}> fontSize="xs"
<VStack align="end" spacing={0}> fontWeight="medium"
<Text fontSize="xs" fontWeight="bold" color={changeColor}> color="rgba(255, 255, 255, 0.9)"
{quote?.current_price?.toFixed(2) || stock.current_price || '--'} noOfLines={1}
</Text> >
<Text fontSize="10px" color={changeColor}> {stock.stock_name || stock.stock_code}
{changePercent !== undefined && changePercent !== null </Text>
? `${isUp ? '+' : ''}${Number(changePercent).toFixed(2)}%` <Text fontSize="10px" color="rgba(255, 255, 255, 0.4)">
: '--'} {stock.stock_code}
</Text> </Text>
</VStack> </VStack>
<Box onClick={(e) => e.stopPropagation()}> <HStack spacing={1}>
<FavoriteButton <VStack align="end" spacing={0}>
isFavorite={true} <Text fontSize="xs" fontWeight="bold" color={changeColor}>
isLoading={isRemoving} {quote?.current_price?.toFixed(2) || stock.current_price || '--'}
onClick={() => handleUnwatch(stock.stock_code)} </Text>
size="sm" <Text fontSize="10px" color={changeColor}>
colorScheme="gold" {changePercent !== undefined && changePercent !== null
showTooltip={true} ? `${isUp ? '+' : ''}${Number(changePercent).toFixed(2)}%`
/> : '--'}
</Box> </Text>
</HStack> </VStack>
</HStack> <Box onClick={(e) => e.stopPropagation()}>
{/* 第二行:日均、周涨 Badge */} <FavoriteButton
{(dailyChg || weeklyChg) && ( isFavorite={true}
<HStack spacing={1.5} fontSize="10px"> isLoading={isRemoving}
{dailyChg && ( onClick={() => handleUnwatch(stock.stock_code)}
<Badge size="sm"
bg="rgba(255, 255, 255, 0.08)" colorScheme="gold"
color={dailyChg.color} showTooltip={true}
fontSize="9px" />
fontWeight="medium" </Box>
px={1.5} </HStack>
py={0.5} </HStack>
borderRadius="sm" {/* 第二行:日均、周涨 Badge */}
> {(dailyChg || weeklyChg) && (
日均 {dailyChg.text} <HStack spacing={1.5} fontSize="10px">
</Badge> {dailyChg && (
<Badge
bg="rgba(255, 255, 255, 0.08)"
color={dailyChg.color}
fontSize="9px"
fontWeight="medium"
px={1.5}
py={0.5}
borderRadius="sm"
>
日均 {dailyChg.text}
</Badge>
)}
{weeklyChg && (
<Badge
bg="rgba(255, 255, 255, 0.08)"
color={weeklyChg.color}
fontSize="9px"
fontWeight="medium"
px={1.5}
py={0.5}
borderRadius="sm"
>
周涨 {weeklyChg.text}
</Badge>
)}
</HStack>
)} )}
{weeklyChg && ( </Box>
<Badge </PopoverTrigger>
bg="rgba(255, 255, 255, 0.08)" <Portal>
color={weeklyChg.color} <PopoverContent
fontSize="9px" bg="rgba(26, 32, 44, 0.98)"
fontWeight="medium" borderColor="rgba(59, 130, 246, 0.3)"
px={1.5} borderWidth="1px"
py={0.5} borderRadius="lg"
borderRadius="sm" boxShadow="0 4px 20px rgba(0, 0, 0, 0.4)"
w="280px"
_focus={{ outline: 'none' }}
>
<PopoverBody p={3}>
{/* 股票信息头部 */}
<HStack justify="space-between" mb={2}>
<VStack align="start" spacing={0}>
<Text fontSize="sm" fontWeight="bold" color="white">
{stock.stock_name}
</Text>
<Text fontSize="xs" color="rgba(255, 255, 255, 0.5)">
{stock.stock_code}
</Text>
</VStack>
<VStack align="end" spacing={0}>
<Text fontSize="md" fontWeight="bold" color={changeColor}>
{quote?.current_price?.toFixed(2) || stock.current_price || '--'}
</Text>
<Text fontSize="sm" fontWeight="medium" color={changeColor}>
{changePercent !== undefined && changePercent !== null
? `${isUp ? '+' : ''}${Number(changePercent).toFixed(2)}%`
: '--'}
</Text>
</VStack>
</HStack>
{/* 分时图 */}
<Box
h="100px"
bg="rgba(15, 15, 22, 0.6)"
borderRadius="md"
p={1}
border="1px solid rgba(59, 130, 246, 0.2)"
> >
周涨 {weeklyChg.text} <MiniTimelineChart stockCode={stock.stock_code} />
</Badge> </Box>
)} {/* 提示文字 */}
</HStack> <Text
)} fontSize="10px"
</Box> color="rgba(255, 255, 255, 0.4)"
textAlign="center"
mt={2}
>
点击查看详情
</Text>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
); );
})} })}
</VStack> </VStack>