update pay ui

This commit is contained in:
2025-12-17 17:29:08 +08:00
parent 8def7f355b
commit 697c366e88
7 changed files with 850 additions and 473 deletions

View 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;