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:
zdl
2026-01-04 17:35:33 +08:00
parent cc16a0052a
commit 365a30da2e
2 changed files with 121 additions and 91 deletions

View File

@@ -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>
) : (

View File

@@ -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>
);