update pay promo

This commit is contained in:
2026-02-03 14:05:52 +08:00
parent 5b54d5e450
commit 390f6024f4
4 changed files with 303 additions and 24 deletions

View File

@@ -0,0 +1,221 @@
/**
* AddStockModal - 添加自选股弹窗
*
* 支持搜索股票并添加到自选股
*/
import React, { useCallback, useState } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
VStack,
HStack,
Text,
Box,
Spinner,
Icon,
IconButton,
useToast,
} from '@chakra-ui/react';
import { Search, X, Plus, Check } from 'lucide-react';
import { useStockSearch } from '@hooks/useStockSearch';
import { useWatchlist } from '@hooks/useWatchlist';
const AddStockModal = ({ isOpen, onClose }) => {
const toast = useToast();
const [addingCode, setAddingCode] = useState(null);
// 搜索相关
const {
searchQuery,
searchResults,
isSearching,
handleSearch,
clearSearch,
} = useStockSearch({ limit: 15, debounceMs: 300 });
// 自选股相关
const { handleAddToWatchlist, isInWatchlist } = useWatchlist();
// 添加股票
const handleAdd = useCallback(async (stock) => {
if (addingCode) return;
setAddingCode(stock.stock_code);
try {
const success = await handleAddToWatchlist(stock.stock_code, stock.stock_name);
if (success) {
// 添加成功后不关闭弹窗,用户可以继续添加
}
} finally {
setAddingCode(null);
}
}, [addingCode, handleAddToWatchlist]);
// 关闭弹窗时清空搜索
const handleClose = useCallback(() => {
clearSearch();
onClose();
}, [clearSearch, onClose]);
return (
<Modal isOpen={isOpen} onClose={handleClose} size="md" isCentered>
<ModalOverlay bg="blackAlpha.700" backdropFilter="blur(4px)" />
<ModalContent
bg="rgba(26, 32, 44, 0.98)"
border="1px solid"
borderColor="rgba(212, 175, 55, 0.3)"
borderRadius="xl"
mx={4}
>
<ModalHeader
pb={2}
borderBottom="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
>
<Text color="white" fontSize="md" fontWeight="bold">
添加自选股
</Text>
</ModalHeader>
<ModalCloseButton color="gray.400" _hover={{ color: 'white' }} />
<ModalBody py={4}>
<VStack spacing={4} align="stretch">
{/* 搜索框 */}
<InputGroup>
<InputLeftElement pointerEvents="none">
<Icon as={Search} color="gray.400" boxSize={4} />
</InputLeftElement>
<Input
placeholder="输入股票代码或名称搜索..."
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
bg="rgba(255, 255, 255, 0.05)"
border="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
color="white"
_placeholder={{ color: 'gray.500' }}
_hover={{ borderColor: 'rgba(212, 175, 55, 0.5)' }}
_focus={{
borderColor: '#D4AF37',
boxShadow: '0 0 0 1px rgba(212, 175, 55, 0.3)',
}}
autoFocus
/>
{(searchQuery || isSearching) && (
<InputRightElement>
{isSearching ? (
<Spinner size="sm" color="#D4AF37" />
) : (
<IconButton
size="xs"
variant="ghost"
icon={<X size={14} />}
onClick={clearSearch}
aria-label="清除搜索"
color="gray.400"
_hover={{ color: 'white', bg: 'transparent' }}
/>
)}
</InputRightElement>
)}
</InputGroup>
{/* 搜索结果 */}
<Box
maxH="350px"
overflowY="auto"
css={{
'&::-webkit-scrollbar': { width: '4px' },
'&::-webkit-scrollbar-track': { background: 'transparent' },
'&::-webkit-scrollbar-thumb': {
background: 'rgba(255, 255, 255, 0.2)',
borderRadius: '2px',
},
}}
>
{searchResults.length > 0 ? (
<VStack spacing={1} align="stretch">
{searchResults.map((stock) => {
const inWatchlist = isInWatchlist(stock.stock_code);
const isAdding = addingCode === stock.stock_code;
return (
<HStack
key={stock.stock_code}
p={3}
bg="rgba(255, 255, 255, 0.03)"
borderRadius="md"
justify="space-between"
_hover={{ bg: 'rgba(255, 255, 255, 0.08)' }}
transition="all 0.15s"
>
<VStack align="start" spacing={0} flex={1}>
<Text color="white" fontSize="sm" fontWeight="medium">
{stock.stock_name}
</Text>
<HStack spacing={2}>
<Text color="gray.400" fontSize="xs">
{stock.stock_code}
</Text>
{stock.pinyin_abbr && (
<Text color="gray.500" fontSize="xs">
({stock.pinyin_abbr.toUpperCase()})
</Text>
)}
</HStack>
</VStack>
{inWatchlist ? (
<HStack spacing={1} color="green.400">
<Icon as={Check} boxSize={4} />
<Text fontSize="xs">已添加</Text>
</HStack>
) : (
<IconButton
size="sm"
variant="outline"
borderColor="#D4AF37"
color="#D4AF37"
icon={isAdding ? <Spinner size="xs" /> : <Plus size={16} />}
onClick={() => handleAdd(stock)}
isDisabled={isAdding}
_hover={{ bg: 'rgba(212, 175, 55, 0.15)' }}
aria-label="添加到自选股"
/>
)}
</HStack>
);
})}
</VStack>
) : searchQuery ? (
<Box py={8} textAlign="center">
<Text color="gray.500" fontSize="sm">
{isSearching ? '搜索中...' : '未找到相关股票'}
</Text>
</Box>
) : (
<Box py={8} textAlign="center">
<Icon as={Search} boxSize={8} color="gray.600" mb={2} />
<Text color="gray.500" fontSize="sm">
输入股票代码或名称开始搜索
</Text>
</Box>
)}
</Box>
</VStack>
</ModalBody>
</ModalContent>
</Modal>
);
};
export default AddStockModal;

View File

@@ -28,11 +28,11 @@ export function SearchBar(props) {
const navigate = useNavigate();
const containerRef = useRef(null);
// 颜色配置 - 固定使用深色主题
const searchIconColor = "gray.400";
const inputBg = "whiteAlpha.100";
// 颜色配置 - 固定使用深色主题(增强可见性)
const searchIconColor = "#D4AF37";
const inputBg = "rgba(26, 32, 44, 0.9)";
const dropdownBg = "#1a1a2e";
const borderColor = "rgba(212, 175, 55, 0.3)";
const borderColor = "rgba(212, 175, 55, 0.5)";
const hoverBg = "whiteAlpha.100";
const textColor = "white";
const subTextColor = "whiteAlpha.600";
@@ -78,22 +78,29 @@ export function SearchBar(props) {
return (
<Box ref={containerRef} position="relative" {...rest}>
<InputGroup borderRadius="8px" w="220px">
<InputGroup borderRadius="8px" w={{ base: "180px", md: "220px", lg: "280px" }}>
<InputLeftElement pointerEvents="none">
<Search color={searchIconColor} size={15} />
<Search color={searchIconColor} size={16} />
</InputLeftElement>
<Input
variant="search"
fontSize="sm"
bg={inputBg}
placeholder="搜索股票..."
color="white"
placeholder="搜索股票代码/名称..."
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
onKeyDown={handleKeyDown}
onFocus={() => searchQuery && searchResults.length > 0 && setShowResults(true)}
border="1px solid"
borderColor={borderColor}
_hover={{ borderColor: accentColor }}
_focus={{ borderColor: accentColor, boxShadow: `0 0 0 1px ${accentColor}` }}
_placeholder={{ color: "gray.400" }}
_hover={{ borderColor: accentColor, bg: "rgba(26, 32, 44, 1)" }}
_focus={{
borderColor: accentColor,
boxShadow: `0 0 0 1px ${accentColor}`,
bg: "rgba(26, 32, 44, 1)"
}}
/>
{(searchQuery || isSearching) && (
<InputRightElement>

View File

@@ -8,13 +8,13 @@
*
* 组件结构:
* - StockHeader股票名称、代码、行业标签、操作按钮
* - PriceDisplay当前价格、涨跌幅 Badge
* - SecondaryQuote今开、昨收、最高、最低
* - PriceDisplay当前价格、涨跌幅 Badge(实时更新)
* - SecondaryQuote今开、昨收、最高、最低(实时更新)
* - GlassSection + MetricRow估值指标、市值股本
* - MainForceInfo主力动态
*/
import React, { memo, useCallback } from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import { Box, Flex, VStack, useDisclosure, useToast } from '@chakra-ui/react';
import { CardGlow } from '@components/FUI';
@@ -35,6 +35,7 @@ import { glassCardStyle } from './components/theme';
// Hooks
import { useStockQuoteData, useStockCompare } from './hooks';
import { useRealtimeQuote } from '@views/StockOverview/components/FlexScreen/hooks';
import type { StockQuoteCardProps } from './types';
const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
@@ -52,6 +53,45 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
clearCompare,
} = useStockCompare(stockCode);
// 实时行情订阅
const subscribedCodes = useMemo(() => {
if (!stockCode) return [];
// 标准化股票代码,添加交易所后缀
const baseCode = stockCode.split('.')[0];
// 根据代码判断交易所
const isShanghai = baseCode.startsWith('6') || baseCode.startsWith('5');
return [isShanghai ? `${baseCode}.SH` : `${baseCode}.SZ`];
}, [stockCode]);
const { quotes } = useRealtimeQuote(subscribedCodes);
// 获取当前股票的实时行情数据
const realtimeQuote = useMemo(() => {
if (!stockCode || !quotes) return null;
const baseCode = stockCode.split('.')[0];
return quotes[`${baseCode}.SH`] || quotes[`${baseCode}.SZ`] || null;
}, [quotes, stockCode]);
// 合并实时数据和静态数据
const displayData = useMemo(() => {
if (!quoteData) return null;
// 如果有实时数据,用实时数据覆盖价格相关字段
if (realtimeQuote && realtimeQuote.price > 0) {
return {
...quoteData,
currentPrice: realtimeQuote.price,
changePercent: realtimeQuote.changePct,
todayOpen: realtimeQuote.open || quoteData.todayOpen,
todayHigh: realtimeQuote.high || quoteData.todayHigh,
todayLow: realtimeQuote.low || quoteData.todayLow,
yesterdayClose: realtimeQuote.prevClose || quoteData.yesterdayClose,
};
}
return quoteData;
}, [quoteData, realtimeQuote]);
const { isOpen: isCompareModalOpen, onOpen: openCompareModal, onClose: closeCompareModal } = useDisclosure();
const handleCompare = (compareCode: string) => {
@@ -93,7 +133,7 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
}, [stockCode, toast]);
// 加载中
if (isLoading || !quoteData) {
if (isLoading || !displayData) {
return <LoadingSkeleton />;
}
@@ -119,18 +159,18 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
onCompare={handleCompare}
/>
{/* ========== 价格区域 ========== */}
{/* ========== 价格区域(实时更新)========== */}
<PriceDisplay
currentPrice={quoteData.currentPrice}
changePercent={quoteData.changePercent}
currentPrice={displayData.currentPrice}
changePercent={displayData.changePercent}
/>
{/* ========== 次要行情 ========== */}
{/* ========== 次要行情(实时更新)========== */}
<SecondaryQuote
todayOpen={quoteData.todayOpen}
yesterdayClose={quoteData.yesterdayClose}
todayHigh={quoteData.todayHigh}
todayLow={quoteData.todayLow}
todayOpen={displayData.todayOpen}
yesterdayClose={displayData.yesterdayClose}
todayHigh={displayData.todayHigh}
todayLow={displayData.todayLow}
/>
{/* ========== 数据区块(三列布局)========== */}

View File

@@ -1,8 +1,9 @@
// 关注股票面板 - 紧凑版
import React, { useState } from 'react';
import { Box, Text, VStack, HStack, Icon, Badge } from '@chakra-ui/react';
import { Box, Text, VStack, HStack, Icon, Badge, useDisclosure } from '@chakra-ui/react';
import { BarChart2, Plus } from 'lucide-react';
import FavoriteButton from '@/components/FavoriteButton';
import AddStockModal from '@/components/AddStockModal';
/**
* 格式化涨跌幅
@@ -26,6 +27,7 @@ const WatchlistPanel = ({
hideTitle = false,
}) => {
const [removingCode, setRemovingCode] = useState(null);
const { isOpen: isAddModalOpen, onOpen: onAddModalOpen, onClose: onAddModalClose } = useDisclosure();
const handleUnwatch = async (stockCode) => {
if (removingCode) return;
@@ -36,6 +38,12 @@ const WatchlistPanel = ({
setRemovingCode(null);
}
};
// 点击加号按钮 - 打开添加自选股弹窗
const handleAddClick = () => {
onAddModalOpen();
};
return (
<Box>
{/* 标题 - 可隐藏 */}
@@ -56,7 +64,7 @@ const WatchlistPanel = ({
color="rgba(255, 255, 255, 0.5)"
cursor="pointer"
_hover={{ color: 'rgba(212, 175, 55, 0.9)' }}
onClick={onAddStock}
onClick={handleAddClick}
/>
</HStack>
)}
@@ -67,7 +75,7 @@ const WatchlistPanel = ({
py={4}
textAlign="center"
cursor="pointer"
onClick={onAddStock}
onClick={handleAddClick}
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
borderRadius="md"
>
@@ -190,6 +198,9 @@ const WatchlistPanel = ({
</VStack>
</Box>
)}
{/* 添加自选股弹窗 */}
<AddStockModal isOpen={isAddModalOpen} onClose={onAddModalClose} />
</Box>
);
};