update pay promo
This commit is contained in:
221
src/components/AddStockModal/index.js
Normal file
221
src/components/AddStockModal/index.js
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
{/* ========== 数据区块(三列布局)========== */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user