update pay ui
This commit is contained in:
288
src/views/Company/components/CompanyHeader/index.tsx
Normal file
288
src/views/Company/components/CompanyHeader/index.tsx
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* Company 页面顶部搜索栏组件
|
||||
* - 显示股票代码、名称、价格、涨跌幅
|
||||
* - 股票搜索功能
|
||||
* - 自选股操作
|
||||
*/
|
||||
|
||||
import React, { memo, useMemo, useCallback, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
VStack,
|
||||
Text,
|
||||
Button,
|
||||
Icon,
|
||||
Badge,
|
||||
Skeleton,
|
||||
} from '@chakra-ui/react';
|
||||
import { AutoComplete, Spin } from 'antd';
|
||||
import { Search, Star } from 'lucide-react';
|
||||
import { useStockSearch } from '@hooks/useStockSearch';
|
||||
import { THEME, getSearchBoxStyles } from '../../config';
|
||||
import type { CompanyHeaderProps, StockSearchResult } from '../../types';
|
||||
|
||||
/**
|
||||
* 股票信息展示组件
|
||||
*/
|
||||
const StockInfoDisplay = memo<{
|
||||
stockCode: string;
|
||||
stockName?: string;
|
||||
price?: number | null;
|
||||
change?: number | null;
|
||||
loading: boolean;
|
||||
}>(({ stockCode, stockName, price, change, loading }) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<VStack align="start" spacing={1}>
|
||||
<Skeleton height="32px" width="200px" />
|
||||
<Skeleton height="24px" width="150px" />
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack align="start" spacing={0}>
|
||||
<HStack spacing={2}>
|
||||
<Text
|
||||
fontSize="2xl"
|
||||
fontWeight="bold"
|
||||
color={THEME.gold}
|
||||
letterSpacing="wider"
|
||||
>
|
||||
{stockCode}
|
||||
</Text>
|
||||
{stockName && (
|
||||
<Text fontSize="xl" fontWeight="medium" color={THEME.textPrimary}>
|
||||
{stockName}
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
{price !== null && price !== undefined && (
|
||||
<HStack spacing={3} mt={1}>
|
||||
<Text fontSize="lg" fontWeight="bold" color={THEME.textPrimary}>
|
||||
¥{price.toFixed(2)}
|
||||
</Text>
|
||||
{change !== null && change !== undefined && (
|
||||
<Badge
|
||||
px={2}
|
||||
py={0.5}
|
||||
borderRadius="md"
|
||||
bg={change >= 0 ? THEME.positiveBg : THEME.negativeBg}
|
||||
color={change >= 0 ? THEME.positive : THEME.negative}
|
||||
fontSize="sm"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{change >= 0 ? '+' : ''}{change.toFixed(2)}%
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
});
|
||||
|
||||
StockInfoDisplay.displayName = 'StockInfoDisplay';
|
||||
|
||||
/**
|
||||
* 搜索操作区组件
|
||||
*/
|
||||
const SearchActions = memo<{
|
||||
inputCode: string;
|
||||
onInputChange: (value: string) => void;
|
||||
onSearch: () => void;
|
||||
onSelect: (value: string) => void;
|
||||
isInWatchlist: boolean;
|
||||
watchlistLoading: boolean;
|
||||
onWatchlistToggle: () => void;
|
||||
}>(({
|
||||
inputCode,
|
||||
onInputChange,
|
||||
onSearch,
|
||||
onSelect,
|
||||
isInWatchlist,
|
||||
watchlistLoading,
|
||||
onWatchlistToggle,
|
||||
}) => {
|
||||
// 股票搜索 Hook
|
||||
const {
|
||||
searchResults,
|
||||
isSearching,
|
||||
handleSearch: doSearch,
|
||||
clearSearch,
|
||||
} = useStockSearch({
|
||||
limit: 10,
|
||||
debounceMs: 300,
|
||||
});
|
||||
|
||||
// 转换为 AutoComplete options
|
||||
const stockOptions = useMemo(() => {
|
||||
return searchResults.map((stock: StockSearchResult) => ({
|
||||
value: stock.stock_code,
|
||||
label: (
|
||||
<Flex justify="space-between" align="center" py={1}>
|
||||
<HStack spacing={2}>
|
||||
<Text fontWeight="bold" color={THEME.gold}>{stock.stock_code}</Text>
|
||||
<Text color={THEME.textPrimary}>{stock.stock_name}</Text>
|
||||
</HStack>
|
||||
{stock.pinyin_abbr && (
|
||||
<Text fontSize="xs" color={THEME.textMuted}>
|
||||
{stock.pinyin_abbr.toUpperCase()}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
),
|
||||
}));
|
||||
}, [searchResults]);
|
||||
|
||||
// 选中股票
|
||||
const handleSelect = useCallback((value: string) => {
|
||||
clearSearch();
|
||||
onSelect(value);
|
||||
}, [clearSearch, onSelect]);
|
||||
|
||||
// 键盘事件
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
onSearch();
|
||||
}
|
||||
}, [onSearch]);
|
||||
|
||||
return (
|
||||
<HStack spacing={3}>
|
||||
{/* 搜索框 */}
|
||||
<Box sx={getSearchBoxStyles(THEME)}>
|
||||
<AutoComplete
|
||||
value={inputCode}
|
||||
options={stockOptions}
|
||||
onSearch={doSearch}
|
||||
onSelect={handleSelect}
|
||||
onChange={onInputChange}
|
||||
placeholder="输入代码、名称或拼音"
|
||||
style={{ width: 220 }}
|
||||
notFoundContent={isSearching ? <Spin size="small" /> : null}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 搜索按钮 */}
|
||||
<Button
|
||||
bg={THEME.gold}
|
||||
color={THEME.bg}
|
||||
_hover={{ bg: THEME.goldLight }}
|
||||
_active={{ bg: THEME.goldDark }}
|
||||
size="md"
|
||||
onClick={onSearch}
|
||||
leftIcon={<Icon as={Search} boxSize={4} />}
|
||||
fontWeight="bold"
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
|
||||
{/* 自选按钮 */}
|
||||
<Button
|
||||
variant={isInWatchlist ? 'solid' : 'outline'}
|
||||
bg={isInWatchlist ? THEME.gold : 'transparent'}
|
||||
color={isInWatchlist ? THEME.bg : THEME.gold}
|
||||
borderColor={THEME.gold}
|
||||
_hover={{
|
||||
bg: isInWatchlist ? THEME.goldLight : 'rgba(212, 175, 55, 0.1)',
|
||||
}}
|
||||
size="md"
|
||||
onClick={onWatchlistToggle}
|
||||
isLoading={watchlistLoading}
|
||||
leftIcon={
|
||||
<Icon
|
||||
as={Star}
|
||||
boxSize={4}
|
||||
fill={isInWatchlist ? 'currentColor' : 'none'}
|
||||
/>
|
||||
}
|
||||
fontWeight="bold"
|
||||
>
|
||||
{isInWatchlist ? '已自选' : '自选'}
|
||||
</Button>
|
||||
</HStack>
|
||||
);
|
||||
});
|
||||
|
||||
SearchActions.displayName = 'SearchActions';
|
||||
|
||||
/**
|
||||
* Company 页面顶部组件
|
||||
*/
|
||||
const CompanyHeader: React.FC<CompanyHeaderProps> = memo(({
|
||||
stockCode,
|
||||
stockInfo,
|
||||
stockInfoLoading,
|
||||
isInWatchlist,
|
||||
watchlistLoading,
|
||||
onStockChange,
|
||||
onWatchlistToggle,
|
||||
}) => {
|
||||
const [inputCode, setInputCode] = useState(stockCode);
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = useCallback(() => {
|
||||
if (inputCode && inputCode !== stockCode) {
|
||||
onStockChange(inputCode);
|
||||
}
|
||||
}, [inputCode, stockCode, onStockChange]);
|
||||
|
||||
// 处理选中
|
||||
const handleSelect = useCallback((value: string) => {
|
||||
setInputCode(value);
|
||||
if (value !== stockCode) {
|
||||
onStockChange(value);
|
||||
}
|
||||
}, [stockCode, onStockChange]);
|
||||
|
||||
// 同步 stockCode 变化
|
||||
React.useEffect(() => {
|
||||
setInputCode(stockCode);
|
||||
}, [stockCode]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg={THEME.cardBg}
|
||||
borderBottom="1px solid"
|
||||
borderColor={THEME.border}
|
||||
px={6}
|
||||
py={4}
|
||||
>
|
||||
<Flex
|
||||
maxW="container.xl"
|
||||
mx="auto"
|
||||
justify="space-between"
|
||||
align="center"
|
||||
wrap="wrap"
|
||||
gap={4}
|
||||
>
|
||||
{/* 左侧:股票信息 */}
|
||||
<StockInfoDisplay
|
||||
stockCode={stockCode}
|
||||
stockName={stockInfo?.stock_name}
|
||||
price={stockInfo?.close_price}
|
||||
change={stockInfo?.change_pct}
|
||||
loading={stockInfoLoading}
|
||||
/>
|
||||
|
||||
{/* 右侧:搜索和操作 */}
|
||||
<SearchActions
|
||||
inputCode={inputCode}
|
||||
onInputChange={setInputCode}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
isInWatchlist={isInWatchlist}
|
||||
watchlistLoading={watchlistLoading}
|
||||
onWatchlistToggle={onWatchlistToggle}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
CompanyHeader.displayName = 'CompanyHeader';
|
||||
|
||||
export default CompanyHeader;
|
||||
Reference in New Issue
Block a user