更新ios
This commit is contained in:
213
src/components/Navbars/components/NavStockSearch/index.js
Normal file
213
src/components/Navbars/components/NavStockSearch/index.js
Normal 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;
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user