refactor(FlexScreen): 搜索框移到标题栏,默认收起
- 搜索框从内容区移到标题栏右侧,始终可见 - 灵活屏默认收起状态 - 删除废弃的 SearchPanel 组件 - 搜索框边框改为金色
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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 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';
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user