refactor(FlexScreen): 搜索框移到标题栏,默认收起

- 搜索框从内容区移到标题栏右侧,始终可见
  - 灵活屏默认收起状态
  - 删除废弃的 SearchPanel 组件
  - 搜索框边框改为金色
This commit is contained in:
zdl
2026-01-04 17:31:53 +08:00
parent d95b2ff313
commit cc16a0052a
5 changed files with 141 additions and 157 deletions

View File

@@ -1,11 +1,12 @@
/** /**
* FlexScreenHeader - 灵活屏头部组件 * FlexScreenHeader - 灵活屏头部组件
* 包含标题、连接状态、操作按钮 * 包含标题、连接状态、搜索框、操作按钮
*/ */
import React, { memo } from 'react'; import React, { memo } from 'react';
import { import {
Flex, Flex,
HStack, HStack,
VStack,
Heading, Heading,
Text, Text,
IconButton, IconButton,
@@ -13,11 +14,20 @@ import {
Badge, Badge,
Tooltip, Tooltip,
Spacer, Spacer,
Box,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
List,
ListItem,
Spinner,
Center,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { Monitor, Wifi, ChevronDown, ChevronUp } from 'lucide-react'; import { Monitor, Wifi, ChevronDown, ChevronUp, Search, X, Plus } from 'lucide-react';
import type { FlexScreenHeaderProps } from '../types'; import type { FlexScreenHeaderProps, SearchResultItem } from '../types';
import { COLORS, actionButtonStyles, collapseButtonStyles } from '../styles'; import { COLORS, actionButtonStyles, collapseButtonStyles, searchDropdownStyles } from '../styles';
const FlexScreenHeader: React.FC<FlexScreenHeaderProps> = memo(({ const FlexScreenHeader: React.FC<FlexScreenHeaderProps> = memo(({
connectionStatus, connectionStatus,
@@ -26,10 +36,19 @@ const FlexScreenHeader: React.FC<FlexScreenHeaderProps> = memo(({
onToggleCollapse, onToggleCollapse,
onClearWatchlist, onClearWatchlist,
onResetWatchlist, onResetWatchlist,
// 搜索相关
searchQuery,
onSearchQueryChange,
searchResults,
isSearching,
showResults,
onAddSecurity,
onClearSearch,
}) => { }) => {
return ( return (
<Flex align="center" mb={4}> <Flex align="center" mb={4} wrap="wrap" gap={3}>
<HStack spacing={3}> {/* 左侧:标题和状态 */}
<HStack spacing={3} flexShrink={0}>
<Icon as={Monitor} boxSize={6} color={COLORS.accent} /> <Icon as={Monitor} boxSize={6} color={COLORS.accent} />
<Heading size="md" color={COLORS.text}> <Heading size="md" color={COLORS.text}>
@@ -47,8 +66,102 @@ const FlexScreenHeader: React.FC<FlexScreenHeaderProps> = memo(({
</Badge> </Badge>
</Tooltip> </Tooltip>
</HStack> </HStack>
<Spacer /> <Spacer />
<HStack spacing={3}>
{/* 中间:搜索框 */}
<Box position="relative" w={{ base: '100%', md: '280px' }} order={{ base: 3, md: 2 }}>
<InputGroup size="sm">
<InputLeftElement pointerEvents="none">
<Search size={14} color={COLORS.subText} />
</InputLeftElement>
<Input
placeholder="搜索股票/指数..."
value={searchQuery}
onChange={e => onSearchQueryChange(e.target.value)}
bg={COLORS.searchBg}
borderRadius="md"
borderColor="#d4a853"
color={COLORS.text}
fontSize="xs"
_placeholder={{ color: COLORS.subText }}
_hover={{ borderColor: '#e6be6a' }}
_focus={{
borderColor: '#e6be6a',
boxShadow: '0 0 0 1px #d4a853',
}}
/>
{searchQuery && (
<InputRightElement>
<IconButton
size="xs"
icon={<X size={12} />}
variant="ghost"
color={COLORS.subText}
_hover={{ bg: COLORS.hoverBg, color: COLORS.text }}
onClick={onClearSearch}
aria-label="清空"
/>
</InputRightElement>
)}
</InputGroup>
{/* 搜索结果下拉 */}
{showResults && (
<Box {...searchDropdownStyles} zIndex={100}>
{isSearching ? (
<Center p={3}>
<Spinner size="sm" color={COLORS.accent} />
</Center>
) : searchResults.length > 0 ? (
<List spacing={0}>
{searchResults.map((stock: SearchResultItem, index: number) => (
<ListItem
key={`${stock.stock_code}-${stock.isIndex ? 'index' : 'stock'}`}
px={3}
py={2}
cursor="pointer"
_hover={{ bg: COLORS.hoverBg }}
onClick={() => onAddSecurity(stock)}
borderBottomWidth={index < searchResults.length - 1 ? '1px' : '0'}
borderColor={COLORS.border}
>
<HStack justify="space-between">
<VStack align="start" spacing={0}>
<HStack spacing={1}>
<Text fontSize="xs" fontWeight="medium" color={COLORS.text}>
{stock.stock_name}
</Text>
<Badge
colorScheme={stock.isIndex ? 'purple' : 'blue'}
fontSize="2xs"
variant="subtle"
>
{stock.isIndex ? '指数' : '股票'}
</Badge>
</HStack>
<Text fontSize="2xs" color={COLORS.subText}>
{stock.stock_code}
</Text>
</VStack>
<Icon as={Plus} boxSize={3} color={COLORS.accent} />
</HStack>
</ListItem>
))}
</List>
) : (
<Center p={3}>
<Text color={COLORS.subText} fontSize="xs">
</Text>
</Center>
)}
</Box>
)}
</Box>
{/* 右侧:操作按钮 */}
<HStack spacing={3} order={{ base: 2, md: 3 }} flexShrink={0}>
{/* 清空列表 */} {/* 清空列表 */}
<Text <Text
{...actionButtonStyles} {...actionButtonStyles}

View File

@@ -1,128 +0,0 @@
/**
* SearchPanel - 搜索面板组件
* 包含搜索输入框和搜索结果下拉列表
*/
import React, { memo } from 'react';
import {
Box,
VStack,
HStack,
Text,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
IconButton,
Collapse,
List,
ListItem,
Spinner,
Center,
Badge,
} from '@chakra-ui/react';
import { Search, X, Plus } from 'lucide-react';
import type { SearchPanelProps } from '../types';
import { COLORS, searchInputStyles, searchDropdownStyles } from '../styles';
const SearchPanel: React.FC<SearchPanelProps> = memo(({
searchQuery,
onSearchQueryChange,
searchResults,
isSearching,
showResults,
onAddSecurity,
onClearSearch,
}) => {
return (
<Box position="relative" mb={4}>
<InputGroup size="md">
<InputLeftElement pointerEvents="none">
<Search size={16} color={COLORS.subText} />
</InputLeftElement>
<Input
placeholder="搜索股票/指数代码或名称..."
value={searchQuery}
onChange={e => onSearchQueryChange(e.target.value)}
{...searchInputStyles}
/>
{searchQuery && (
<InputRightElement>
<IconButton
size="sm"
icon={<X size={16} />}
variant="ghost"
color={COLORS.subText}
_hover={{ bg: COLORS.hoverBg, color: COLORS.text }}
onClick={onClearSearch}
aria-label="清空"
/>
</InputRightElement>
)}
</InputGroup>
{/* 搜索结果下拉 */}
<Collapse in={showResults} animateOpacity>
<Box {...searchDropdownStyles}>
{isSearching ? (
<Center p={4}>
<Spinner size="sm" color={COLORS.accent} />
</Center>
) : searchResults.length > 0 ? (
<List spacing={0}>
{searchResults.map((stock, index) => (
<ListItem
key={`${stock.stock_code}-${stock.isIndex ? 'index' : 'stock'}`}
px={4}
py={2}
cursor="pointer"
_hover={{ bg: COLORS.hoverBg }}
onClick={() => onAddSecurity(stock)}
borderBottomWidth={index < searchResults.length - 1 ? '1px' : '0'}
borderColor={COLORS.border}
>
<HStack justify="space-between">
<VStack align="start" spacing={0}>
<HStack spacing={2}>
<Text fontWeight="medium" color={COLORS.text}>
{stock.stock_name}
</Text>
<Badge
colorScheme={stock.isIndex ? 'purple' : 'blue'}
fontSize="xs"
variant="subtle"
>
{stock.isIndex ? '指数' : '股票'}
</Badge>
</HStack>
<Text fontSize="xs" color={COLORS.subText}>
{stock.stock_code}
</Text>
</VStack>
<IconButton
icon={<Plus size={16} />}
size="xs"
colorScheme="purple"
variant="ghost"
aria-label="添加"
/>
</HStack>
</ListItem>
))}
</List>
) : (
<Center p={4}>
<Text color={COLORS.subText} fontSize="sm">
</Text>
</Center>
)}
</Box>
</Collapse>
</Box>
);
});
SearchPanel.displayName = 'SearchPanel';
export default SearchPanel;

View File

@@ -6,5 +6,4 @@ export { default as MiniTimelineChart } from './MiniTimelineChart';
export { default as OrderBookPanel } from './OrderBookPanel'; export { default as OrderBookPanel } from './OrderBookPanel';
export { default as QuoteTile } from './QuoteTile'; export { default as QuoteTile } from './QuoteTile';
export { default as FlexScreenHeader } from './FlexScreenHeader'; export { default as FlexScreenHeader } from './FlexScreenHeader';
export { default as SearchPanel } from './SearchPanel';
export { default as HotRecommendations } from './HotRecommendations'; export { default as HotRecommendations } from './HotRecommendations';

View File

@@ -17,7 +17,6 @@ import {
Text, Text,
SimpleGrid, SimpleGrid,
Icon, Icon,
Collapse,
Center, Center,
useToast, useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
@@ -28,7 +27,6 @@ import { getFullCode } from './hooks/utils';
import { import {
QuoteTile, QuoteTile,
FlexScreenHeader, FlexScreenHeader,
SearchPanel,
HotRecommendations, HotRecommendations,
} from './components'; } from './components';
import { logger } from '@utils/logger'; import { logger } from '@utils/logger';
@@ -62,7 +60,7 @@ const FlexScreen: React.FC = () => {
const [searchResults, setSearchResults] = useState<SearchResultItem[]>([]); const [searchResults, setSearchResults] = useState<SearchResultItem[]>([]);
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
const [showResults, setShowResults] = useState(false); const [showResults, setShowResults] = useState(false);
// 面板状态 // 面板状态(默认收起)
const [isCollapsed, setIsCollapsed] = useState(true); const [isCollapsed, setIsCollapsed] = useState(true);
// 获取订阅的证券代码列表 // 获取订阅的证券代码列表
@@ -240,7 +238,7 @@ const FlexScreen: React.FC = () => {
borderRadius="16px" borderRadius="16px"
> >
<CardBody> <CardBody>
{/* 头部 */} {/* 头部(含搜索框) */}
<FlexScreenHeader <FlexScreenHeader
connectionStatus={connectionStatus} connectionStatus={connectionStatus}
isAnyConnected={isAnyConnected} isAnyConnected={isAnyConnected}
@@ -248,25 +246,19 @@ const FlexScreen: React.FC = () => {
onToggleCollapse={toggleCollapse} onToggleCollapse={toggleCollapse}
onClearWatchlist={clearWatchlist} onClearWatchlist={clearWatchlist}
onResetWatchlist={resetWatchlist} onResetWatchlist={resetWatchlist}
searchQuery={searchQuery}
onSearchQueryChange={setSearchQuery}
searchResults={searchResults}
isSearching={isSearching}
showResults={showResults}
onAddSecurity={addSecurity}
onClearSearch={clearSearch}
/> />
{/* 搜索框 - 仅展开时显示 */} {/* 热门推荐 - 仅展开且列表为空时显示 */}
<Collapse in={!isCollapsed} animateOpacity> {!isCollapsed && watchlist.length === 0 && (
<SearchPanel <HotRecommendations onAddSecurity={addSecurity} />
searchQuery={searchQuery} )}
onSearchQueryChange={setSearchQuery}
searchResults={searchResults}
isSearching={isSearching}
showResults={showResults}
onAddSecurity={addSecurity}
onClearSearch={clearSearch}
/>
{/* 热门推荐 - 仅展开且列表为空时显示 */}
{watchlist.length === 0 && (
<HotRecommendations onAddSecurity={addSecurity} />
)}
</Collapse>
{/* 自选列表 */} {/* 自选列表 */}
{watchlist.length > 0 ? ( {watchlist.length > 0 ? (

View File

@@ -407,6 +407,14 @@ export interface FlexScreenHeaderProps {
onToggleCollapse: () => void; onToggleCollapse: () => void;
onClearWatchlist: () => void; onClearWatchlist: () => void;
onResetWatchlist: () => void; onResetWatchlist: () => void;
// 搜索相关
searchQuery: string;
onSearchQueryChange: (query: string) => void;
searchResults: SearchResultItem[];
isSearching: boolean;
showResults: boolean;
onAddSecurity: (security: SearchResultItem) => void;
onClearSearch: () => void;
} }
/** SearchPanel Props */ /** SearchPanel Props */