refactor(OrderBookPanel): 重构盘口面板为左右结构布局
- 卖盘显示在左侧,买盘显示在右侧,更符合专业交易软件布局 - 调整内容排列顺序:左侧(量-价-档),右侧(档-价-量) - 成交量条方向适配左右布局 - 档位切换按钮居中显示 - FlexScreenHeader 性能优化:提取 SearchResultListItem 子组件,useCallback 优化 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* FlexScreenHeader - 灵活屏头部组件
|
||||
* 包含标题、连接状态、搜索框、操作按钮
|
||||
*/
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback, ChangeEvent } from 'react';
|
||||
import {
|
||||
Flex,
|
||||
HStack,
|
||||
@@ -29,6 +29,49 @@ import { Monitor, Wifi, ChevronDown, ChevronUp, Search, X, Plus } from 'lucide-r
|
||||
import type { FlexScreenHeaderProps, SearchResultItem } from '../types';
|
||||
import { COLORS, actionButtonStyles, collapseButtonStyles, searchDropdownStyles } from '../styles';
|
||||
|
||||
/** 搜索结果列表项 - memo 避免列表重渲染 */
|
||||
const SearchResultListItem = memo<{
|
||||
stock: SearchResultItem;
|
||||
isLast: boolean;
|
||||
onAdd: (stock: SearchResultItem) => void;
|
||||
}>(({ stock, isLast, onAdd }) => {
|
||||
const handleClick = useCallback(() => onAdd(stock), [onAdd, stock]);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
px={3}
|
||||
py={2}
|
||||
cursor="pointer"
|
||||
_hover={{ bg: COLORS.hoverBg }}
|
||||
onClick={handleClick}
|
||||
borderBottomWidth={isLast ? '0' : '1px'}
|
||||
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>
|
||||
);
|
||||
});
|
||||
SearchResultListItem.displayName = 'SearchResultListItem';
|
||||
|
||||
const FlexScreenHeader: React.FC<FlexScreenHeaderProps> = memo(({
|
||||
connectionStatus,
|
||||
isAnyConnected,
|
||||
@@ -45,6 +88,12 @@ const FlexScreenHeader: React.FC<FlexScreenHeaderProps> = memo(({
|
||||
onAddSecurity,
|
||||
onClearSearch,
|
||||
}) => {
|
||||
// 搜索输入处理 - useCallback 避免每次渲染创建新函数
|
||||
const handleSearchChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => onSearchQueryChange(e.target.value),
|
||||
[onSearchQueryChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex align="center" mb={4} wrap="wrap" gap={3}>
|
||||
{/* 左侧:标题和状态 */}
|
||||
@@ -78,7 +127,7 @@ const FlexScreenHeader: React.FC<FlexScreenHeaderProps> = memo(({
|
||||
<Input
|
||||
placeholder="搜索股票/指数..."
|
||||
value={searchQuery}
|
||||
onChange={e => onSearchQueryChange(e.target.value)}
|
||||
onChange={handleSearchChange}
|
||||
bg={COLORS.searchBg}
|
||||
borderRadius="md"
|
||||
borderColor="#d4a853"
|
||||
@@ -116,37 +165,12 @@ const FlexScreenHeader: React.FC<FlexScreenHeaderProps> = memo(({
|
||||
) : searchResults.length > 0 ? (
|
||||
<List spacing={0}>
|
||||
{searchResults.map((stock: SearchResultItem, index: number) => (
|
||||
<ListItem
|
||||
<SearchResultListItem
|
||||
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>
|
||||
stock={stock}
|
||||
isLast={index === searchResults.length - 1}
|
||||
onAdd={onAddSecurity}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
|
||||
@@ -71,10 +71,12 @@ interface OrderRowProps {
|
||||
isBid: boolean;
|
||||
maxVolume: number;
|
||||
isLimitPrice: boolean;
|
||||
/** 是否为左侧布局(卖盘) */
|
||||
isLeft?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单行盘口 - 深色主题
|
||||
* 单行盘口 - 左右结构版本
|
||||
*/
|
||||
const OrderRow: React.FC<OrderRowProps> = ({
|
||||
label,
|
||||
@@ -84,9 +86,10 @@ const OrderRow: React.FC<OrderRowProps> = ({
|
||||
isBid,
|
||||
maxVolume,
|
||||
isLimitPrice,
|
||||
isLeft = false,
|
||||
}) => {
|
||||
// 深色主题颜色
|
||||
const barColor = isBid ? 'rgba(255, 77, 77, 0.2)' : 'rgba(34, 197, 94, 0.2)';
|
||||
const barColor = isBid ? 'rgba(255, 77, 77, 0.25)' : 'rgba(34, 197, 94, 0.25)';
|
||||
const limitColor = 'orange.300';
|
||||
const labelColor = 'rgba(255, 255, 255, 0.5)';
|
||||
const volumeColor = 'rgba(255, 255, 255, 0.6)';
|
||||
@@ -99,17 +102,18 @@ const OrderRow: React.FC<OrderRowProps> = ({
|
||||
|
||||
return (
|
||||
<HStack
|
||||
spacing={2}
|
||||
spacing={1}
|
||||
py={0.5}
|
||||
px={1}
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
fontSize="xs"
|
||||
justify={isLeft ? 'flex-end' : 'flex-start'}
|
||||
>
|
||||
{/* 成交量条 */}
|
||||
{/* 成交量条 - 左侧从右向左,右侧从左向右 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
right={0}
|
||||
{...(isLeft ? { right: 0 } : { left: 0 })}
|
||||
top={0}
|
||||
bottom={0}
|
||||
width={`${barWidth}%`}
|
||||
@@ -117,29 +121,32 @@ const OrderRow: React.FC<OrderRowProps> = ({
|
||||
transition="width 0.2s"
|
||||
/>
|
||||
|
||||
{/* 内容 */}
|
||||
<Text color={labelColor} w="24px" flexShrink={0} zIndex={1}>
|
||||
{label}
|
||||
</Text>
|
||||
<HStack flex={1} justify="flex-end" zIndex={1}>
|
||||
<Text color={isLimitPrice ? limitColor : priceInfo.color} fontWeight="medium">
|
||||
{priceInfo.text}
|
||||
</Text>
|
||||
{isLimitPrice && (
|
||||
<Tooltip label={isBid ? '跌停价' : '涨停价'}>
|
||||
<Badge
|
||||
colorScheme={isBid ? 'green' : 'red'}
|
||||
fontSize="2xs"
|
||||
variant="subtle"
|
||||
>
|
||||
{isBid ? '跌' : '涨'}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HStack>
|
||||
<Text color={volumeColor} w="40px" textAlign="right" zIndex={1}>
|
||||
{volumeText}
|
||||
</Text>
|
||||
{/* 内容 - 左侧:量 价 档位,右侧:档位 价 量 */}
|
||||
{isLeft ? (
|
||||
<>
|
||||
<Text color={volumeColor} fontSize="2xs" zIndex={1} minW="32px" textAlign="left">
|
||||
{volumeText}
|
||||
</Text>
|
||||
<Text color={isLimitPrice ? limitColor : priceInfo.color} fontWeight="medium" zIndex={1} minW="42px" textAlign="right">
|
||||
{priceInfo.text}
|
||||
</Text>
|
||||
<Text color={labelColor} zIndex={1} minW="20px" textAlign="right">
|
||||
{label}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text color={labelColor} zIndex={1} minW="20px" textAlign="left">
|
||||
{label}
|
||||
</Text>
|
||||
<Text color={isLimitPrice ? limitColor : priceInfo.color} fontWeight="medium" zIndex={1} minW="42px" textAlign="left">
|
||||
{priceInfo.text}
|
||||
</Text>
|
||||
<Text color={volumeColor} fontSize="2xs" zIndex={1} minW="32px" textAlign="right">
|
||||
{volumeText}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
@@ -187,9 +194,9 @@ const OrderBookPanel: React.FC<OrderBookPanelProps> = ({
|
||||
const isLowerLimit = (price: number): boolean =>
|
||||
!!lowerLimit && Math.abs(price - lowerLimit) < 0.001;
|
||||
|
||||
// 卖盘(从卖N到卖1,即价格从高到低)
|
||||
// 卖盘(从卖1到卖N,价格从低到高,显示在左侧)
|
||||
const askRows: React.ReactNode[] = [];
|
||||
for (let i = showLevels - 1; i >= 0; i--) {
|
||||
for (let i = 0; i < showLevels; i++) {
|
||||
askRows.push(
|
||||
<OrderRow
|
||||
key={`ask${i + 1}`}
|
||||
@@ -200,11 +207,12 @@ const OrderBookPanel: React.FC<OrderBookPanelProps> = ({
|
||||
isBid={false}
|
||||
maxVolume={maxVolume}
|
||||
isLimitPrice={isUpperLimit(askPrices[i])}
|
||||
isLeft={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 买盘(从买1到买N,即价格从高到低)
|
||||
// 买盘(从买1到买N,价格从高到低,显示在右侧)
|
||||
const bidRows: React.ReactNode[] = [];
|
||||
for (let i = 0; i < showLevels; i++) {
|
||||
bidRows.push(
|
||||
@@ -217,6 +225,7 @@ const OrderBookPanel: React.FC<OrderBookPanelProps> = ({
|
||||
isBid={true}
|
||||
maxVolume={maxVolume}
|
||||
isLimitPrice={isLowerLimit(bidPrices[i])}
|
||||
isLeft={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -238,7 +247,7 @@ const OrderBookPanel: React.FC<OrderBookPanelProps> = ({
|
||||
<VStack spacing={0} align="stretch">
|
||||
{/* 档位切换(只有当有超过5档数据时才显示) */}
|
||||
{maxAvailableLevels > 5 && (
|
||||
<HStack justify="flex-end" mb={1}>
|
||||
<HStack justify="center" mb={1}>
|
||||
<ButtonGroup size="xs" isAttached variant="outline">
|
||||
<Button
|
||||
onClick={() => setShowLevels(5)}
|
||||
@@ -264,36 +273,33 @@ const OrderBookPanel: React.FC<OrderBookPanelProps> = ({
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* 卖盘 */}
|
||||
{askRows}
|
||||
|
||||
{/* 分隔线 + 当前价信息 */}
|
||||
<Box h="1px" bg={borderColor} my={1} position="relative">
|
||||
{prevClose && (
|
||||
<Text
|
||||
position="absolute"
|
||||
right={0}
|
||||
top="50%"
|
||||
transform="translateY(-50%)"
|
||||
fontSize="2xs"
|
||||
color={textColor}
|
||||
bg={bgColor}
|
||||
px={1}
|
||||
>
|
||||
昨收 {prevClose.toFixed(2)}
|
||||
{/* 左右结构:卖盘(左) | 买盘(右) */}
|
||||
<HStack spacing={2} align="flex-start">
|
||||
{/* 左侧 - 卖盘 */}
|
||||
<VStack spacing={0} flex={1} align="stretch">
|
||||
<Text fontSize="2xs" color={textColor} textAlign="center" mb={1}>
|
||||
卖盘
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{askRows}
|
||||
</VStack>
|
||||
|
||||
{/* 买盘 */}
|
||||
{bidRows}
|
||||
{/* 中间分隔线 */}
|
||||
<Box w="1px" bg={borderColor} alignSelf="stretch" />
|
||||
|
||||
{/* 涨跌停价信息 */}
|
||||
{(upperLimit || lowerLimit) && (
|
||||
<HStack justify="space-between" mt={1} fontSize="2xs" color={textColor}>
|
||||
{lowerLimit && <Text>跌停 {lowerLimit.toFixed(2)}</Text>}
|
||||
{upperLimit && <Text>涨停 {upperLimit.toFixed(2)}</Text>}
|
||||
</HStack>
|
||||
{/* 右侧 - 买盘 */}
|
||||
<VStack spacing={0} flex={1} align="stretch">
|
||||
<Text fontSize="2xs" color={textColor} textAlign="center" mb={1}>
|
||||
买盘
|
||||
</Text>
|
||||
{bidRows}
|
||||
</VStack>
|
||||
</HStack>
|
||||
|
||||
{/* 底部昨收信息 */}
|
||||
{prevClose && (
|
||||
<Text fontSize="2xs" color={textColor} textAlign="center" mt={1}>
|
||||
昨收 {prevClose.toFixed(2)}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user