Files
vf_react/src/views/LimitAnalyse/components/SearchComponents.js
2025-12-14 16:43:45 +08:00

320 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import {
Card,
CardBody,
HStack,
Text,
Button,
Input,
InputGroup,
InputLeftElement,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
Badge,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
Tag,
TagLabel,
Wrap,
WrapItem,
useColorModeValue,
useToast,
Tooltip,
Divider,
Alert,
AlertIcon,
} from '@chakra-ui/react';
import { formatTooltipText, getFormattedTextProps } from '../../../utils/textUtils';
import { SearchIcon, DownloadIcon } from '@chakra-ui/icons';
// 简化版搜索组件 - 仅支持股票代码/名称精确匹配
export const AdvancedSearch = ({ onSearch, loading }) => {
const [searchKeyword, setSearchKeyword] = useState('');
const cardBg = useColorModeValue('white', 'gray.800');
const toast = useToast();
const handleSearch = () => {
if (!searchKeyword.trim()) {
toast({
title: '请输入股票代码或名称',
status: 'warning',
duration: 2000,
});
return;
}
const searchParams = {
query: searchKeyword.trim(),
mode: 'exact', // 固定为精确匹配
page: 1,
page_size: 50,
};
onSearch(searchParams);
};
const clearSearch = () => {
setSearchKeyword('');
};
return (
<Card bg={cardBg} borderRadius="xl" boxShadow="xl" mb={6}>
<CardBody>
<HStack w="full" spacing={3}>
<InputGroup size="lg" flex={1}>
<InputLeftElement>
<SearchIcon color="gray.400" />
</InputLeftElement>
<Input
placeholder="输入股票代码或名称搜索(如 600000 或 浦发银行)"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
fontSize="md"
/>
</InputGroup>
<Button
size="lg"
colorScheme="blue"
onClick={handleSearch}
isLoading={loading}
px={8}
leftIcon={<SearchIcon />}
>
搜索
</Button>
<Button
size="lg"
variant="outline"
onClick={clearSearch}
px={6}
>
清空
</Button>
</HStack>
</CardBody>
</Card>
);
};
// 搜索结果弹窗组件
export const SearchResultsModal = ({ isOpen, onClose, searchResults, onStockClick }) => {
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 20;
if (!searchResults) return null;
const { stocks = [], total = 0 } = searchResults;
const totalPages = Math.ceil(total / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentStocks = stocks.slice(startIndex, endIndex);
const exportResults = () => {
const csvContent = [
['股票代码', '股票名称', '涨停时间', '涨停原因', '所属板块'].join(','),
...stocks.map(stock => [
stock.scode,
stock.sname,
stock.zt_time || '-',
(stock.brief || stock.summary || '-').replace(/,/g, ''),
(stock.core_sectors || []).join('')
].join(','))
].join('\n');
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `搜索结果_${new Date().toISOString().split('T')[0]}.csv`;
link.click();
};
return (
<Modal isOpen={isOpen} onClose={onClose} size="6xl">
<ModalOverlay backdropFilter="blur(5px)" />
<ModalContent maxW="90vw" maxH="90vh">
<ModalHeader bg="blue.500" color="white">
<HStack justify="space-between">
<Text>搜索结果</Text>
<HStack spacing={2}>
<Badge bg="whiteAlpha.900" color="blue.500" fontSize="md" px={3}>
共找到 {total} 只股票
</Badge>
<Button
size="sm"
leftIcon={<DownloadIcon />}
onClick={exportResults}
variant="outline"
colorScheme="whiteAlpha"
>
导出CSV
</Button>
</HStack>
</HStack>
</ModalHeader>
<ModalCloseButton color="white" />
<ModalBody overflowY="auto" maxH="70vh" p={6}>
{stocks.length === 0 ? (
<Alert status="warning" borderRadius="md">
<AlertIcon />
没有找到符合条件的股票请尝试调整搜索条件
</Alert>
) : (
<Table variant="simple" size="sm">
<Thead position="sticky" top={0} bg="white" zIndex={1}>
<Tr>
<Th>序号</Th>
<Th>股票代码</Th>
<Th>股票名称</Th>
<Th>涨停时间</Th>
<Th>连板天数</Th>
<Th>涨停原因</Th>
<Th>所属板块</Th>
<Th>K线</Th>
</Tr>
</Thead>
<Tbody>
{currentStocks.map((stock, index) => (
<Tr key={`${stock.scode}-${index}`} _hover={{ bg: 'gray.50' }}>
<Td>{startIndex + index + 1}</Td>
<Td>
<Badge colorScheme="purple">{stock.scode}</Badge>
</Td>
<Td fontWeight="bold">{stock.sname}</Td>
<Td fontSize="sm">{stock.zt_time || '-'}</Td>
<Td>
{stock.continuous_days && (
<Badge
colorScheme={
stock.continuous_days.includes('5') ? 'red' :
stock.continuous_days.includes('3') ? 'orange' :
stock.continuous_days.includes('2') ? 'yellow' :
'green'
}
>
{stock.continuous_days}
</Badge>
)}
</Td>
<Td maxW="300px">
<Tooltip
label={formatTooltipText(stock.brief || stock.summary)}
placement="top"
hasArrow
bg="gray.800"
color="white"
px={3}
py={2}
borderRadius="md"
fontSize="sm"
maxW="400px"
whiteSpace="pre-line"
>
<Text
fontSize="sm"
noOfLines={2}
{...getFormattedTextProps(stock.brief || stock.summary || '-').props}
_hover={{ cursor: 'help' }}
>
{getFormattedTextProps(stock.brief || stock.summary || '-').children}
</Text>
</Tooltip>
</Td>
<Td maxW="200px">
<Wrap spacing={1}>
{(stock.core_sectors || []).slice(0, 3).map((sector, i) => (
<WrapItem key={i}>
<Tag size="sm" colorScheme="teal">
<TagLabel fontSize="xs">{sector}</TagLabel>
</Tag>
</WrapItem>
))}
{stock.core_sectors && stock.core_sectors.length > 3 && (
<WrapItem>
<Tag size="sm" colorScheme="gray">
<TagLabel fontSize="xs">+{stock.core_sectors.length - 3}</TagLabel>
</Tag>
</WrapItem>
)}
</Wrap>
</Td>
<Td>
<Button
size="sm"
colorScheme="blue"
onClick={() => {
// 跳转到公司详情页面查看K线
window.open(`https://valuefrontier.cn/company?scode=${stock.scode}`, '_blank');
}}
>
查看
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
)}
{totalPages > 1 && (
<>
<Divider my={4} />
<HStack justify="center" spacing={2}>
<Button
size="sm"
onClick={() => setCurrentPage(1)}
isDisabled={currentPage === 1}
>
首页
</Button>
<Button
size="sm"
onClick={() => setCurrentPage(currentPage - 1)}
isDisabled={currentPage === 1}
>
上一页
</Button>
<Text fontSize="sm">
{currentPage} / {totalPages}
</Text>
<Button
size="sm"
onClick={() => setCurrentPage(currentPage + 1)}
isDisabled={currentPage === totalPages}
>
下一页
</Button>
<Button
size="sm"
onClick={() => setCurrentPage(totalPages)}
isDisabled={currentPage === totalPages}
>
末页
</Button>
</HStack>
</>
)}
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onClose}>
关闭
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
export default { AdvancedSearch, SearchResultsModal };