refactor(FlexScreen): 搜索框移到标题栏,默认收起
- 搜索框从内容区移到标题栏右侧,始终可见 - 灵活屏默认收起状态 - 删除废弃的 SearchPanel 组件 - 搜索框边框改为金色
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* FlexScreenHeader - 灵活屏头部组件
|
||||
* 包含标题、连接状态、操作按钮
|
||||
* 包含标题、连接状态、搜索框、操作按钮
|
||||
*/
|
||||
import React, { memo } from 'react';
|
||||
import {
|
||||
Flex,
|
||||
HStack,
|
||||
VStack,
|
||||
Heading,
|
||||
Text,
|
||||
IconButton,
|
||||
@@ -13,11 +14,20 @@ import {
|
||||
Badge,
|
||||
Tooltip,
|
||||
Spacer,
|
||||
Box,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
InputRightElement,
|
||||
List,
|
||||
ListItem,
|
||||
Spinner,
|
||||
Center,
|
||||
} 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 { COLORS, actionButtonStyles, collapseButtonStyles } from '../styles';
|
||||
import type { FlexScreenHeaderProps, SearchResultItem } from '../types';
|
||||
import { COLORS, actionButtonStyles, collapseButtonStyles, searchDropdownStyles } from '../styles';
|
||||
|
||||
const FlexScreenHeader: React.FC<FlexScreenHeaderProps> = memo(({
|
||||
connectionStatus,
|
||||
@@ -26,10 +36,19 @@ const FlexScreenHeader: React.FC<FlexScreenHeaderProps> = memo(({
|
||||
onToggleCollapse,
|
||||
onClearWatchlist,
|
||||
onResetWatchlist,
|
||||
// 搜索相关
|
||||
searchQuery,
|
||||
onSearchQueryChange,
|
||||
searchResults,
|
||||
isSearching,
|
||||
showResults,
|
||||
onAddSecurity,
|
||||
onClearSearch,
|
||||
}) => {
|
||||
return (
|
||||
<Flex align="center" mb={4}>
|
||||
<HStack spacing={3}>
|
||||
<Flex align="center" mb={4} wrap="wrap" gap={3}>
|
||||
{/* 左侧:标题和状态 */}
|
||||
<HStack spacing={3} flexShrink={0}>
|
||||
<Icon as={Monitor} boxSize={6} color={COLORS.accent} />
|
||||
<Heading size="md" color={COLORS.text}>
|
||||
灵活屏
|
||||
@@ -47,8 +66,102 @@ const FlexScreenHeader: React.FC<FlexScreenHeaderProps> = memo(({
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
|
||||
<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
|
||||
{...actionButtonStyles}
|
||||
|
||||
@@ -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;
|
||||
@@ -6,5 +6,4 @@ export { default as MiniTimelineChart } from './MiniTimelineChart';
|
||||
export { default as OrderBookPanel } from './OrderBookPanel';
|
||||
export { default as QuoteTile } from './QuoteTile';
|
||||
export { default as FlexScreenHeader } from './FlexScreenHeader';
|
||||
export { default as SearchPanel } from './SearchPanel';
|
||||
export { default as HotRecommendations } from './HotRecommendations';
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
Text,
|
||||
SimpleGrid,
|
||||
Icon,
|
||||
Collapse,
|
||||
Center,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
@@ -28,7 +27,6 @@ import { getFullCode } from './hooks/utils';
|
||||
import {
|
||||
QuoteTile,
|
||||
FlexScreenHeader,
|
||||
SearchPanel,
|
||||
HotRecommendations,
|
||||
} from './components';
|
||||
import { logger } from '@utils/logger';
|
||||
@@ -62,7 +60,7 @@ const FlexScreen: React.FC = () => {
|
||||
const [searchResults, setSearchResults] = useState<SearchResultItem[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [showResults, setShowResults] = useState(false);
|
||||
// 面板状态
|
||||
// 面板状态(默认收起)
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
// 获取订阅的证券代码列表
|
||||
@@ -240,7 +238,7 @@ const FlexScreen: React.FC = () => {
|
||||
borderRadius="16px"
|
||||
>
|
||||
<CardBody>
|
||||
{/* 头部 */}
|
||||
{/* 头部(含搜索框) */}
|
||||
<FlexScreenHeader
|
||||
connectionStatus={connectionStatus}
|
||||
isAnyConnected={isAnyConnected}
|
||||
@@ -248,25 +246,19 @@ const FlexScreen: React.FC = () => {
|
||||
onToggleCollapse={toggleCollapse}
|
||||
onClearWatchlist={clearWatchlist}
|
||||
onResetWatchlist={resetWatchlist}
|
||||
searchQuery={searchQuery}
|
||||
onSearchQueryChange={setSearchQuery}
|
||||
searchResults={searchResults}
|
||||
isSearching={isSearching}
|
||||
showResults={showResults}
|
||||
onAddSecurity={addSecurity}
|
||||
onClearSearch={clearSearch}
|
||||
/>
|
||||
|
||||
{/* 搜索框 - 仅展开时显示 */}
|
||||
<Collapse in={!isCollapsed} animateOpacity>
|
||||
<SearchPanel
|
||||
searchQuery={searchQuery}
|
||||
onSearchQueryChange={setSearchQuery}
|
||||
searchResults={searchResults}
|
||||
isSearching={isSearching}
|
||||
showResults={showResults}
|
||||
onAddSecurity={addSecurity}
|
||||
onClearSearch={clearSearch}
|
||||
/>
|
||||
|
||||
{/* 热门推荐 - 仅展开且列表为空时显示 */}
|
||||
{watchlist.length === 0 && (
|
||||
<HotRecommendations onAddSecurity={addSecurity} />
|
||||
)}
|
||||
</Collapse>
|
||||
{/* 热门推荐 - 仅展开且列表为空时显示 */}
|
||||
{!isCollapsed && watchlist.length === 0 && (
|
||||
<HotRecommendations onAddSecurity={addSecurity} />
|
||||
)}
|
||||
|
||||
{/* 自选列表 */}
|
||||
{watchlist.length > 0 ? (
|
||||
|
||||
@@ -407,6 +407,14 @@ export interface FlexScreenHeaderProps {
|
||||
onToggleCollapse: () => void;
|
||||
onClearWatchlist: () => void;
|
||||
onResetWatchlist: () => void;
|
||||
// 搜索相关
|
||||
searchQuery: string;
|
||||
onSearchQueryChange: (query: string) => void;
|
||||
searchResults: SearchResultItem[];
|
||||
isSearching: boolean;
|
||||
showResults: boolean;
|
||||
onAddSecurity: (security: SearchResultItem) => void;
|
||||
onClearSearch: () => void;
|
||||
}
|
||||
|
||||
/** SearchPanel Props */
|
||||
|
||||
Reference in New Issue
Block a user