423 lines
19 KiB
JavaScript
423 lines
19 KiB
JavaScript
import React, { useState } from 'react';
|
||
import {
|
||
Box,
|
||
Card,
|
||
CardBody,
|
||
VStack,
|
||
HStack,
|
||
Text,
|
||
Button,
|
||
Input,
|
||
InputGroup,
|
||
InputLeftElement,
|
||
Select,
|
||
RadioGroup,
|
||
Radio,
|
||
Modal,
|
||
ModalOverlay,
|
||
ModalContent,
|
||
ModalHeader,
|
||
ModalFooter,
|
||
ModalBody,
|
||
ModalCloseButton,
|
||
Badge,
|
||
Table,
|
||
Thead,
|
||
Tbody,
|
||
Tr,
|
||
Th,
|
||
Td,
|
||
Tag,
|
||
TagLabel,
|
||
Wrap,
|
||
WrapItem,
|
||
IconButton,
|
||
useColorModeValue,
|
||
useToast,
|
||
Tooltip,
|
||
Divider,
|
||
Alert,
|
||
AlertIcon,
|
||
} from '@chakra-ui/react';
|
||
import { formatTooltipText, getFormattedTextProps } from '../../../utils/textUtils';
|
||
import { SearchIcon, CalendarIcon, ViewIcon, ExternalLinkIcon, DownloadIcon } from '@chakra-ui/icons';
|
||
|
||
// 高级搜索组件
|
||
export const AdvancedSearch = ({ onSearch, loading }) => {
|
||
const [searchKeyword, setSearchKeyword] = useState('');
|
||
const [searchMode, setSearchMode] = useState('hybrid');
|
||
const [searchType, setSearchType] = useState('all');
|
||
const [dateRange, setDateRange] = useState({ start: '', end: '' });
|
||
|
||
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,
|
||
mode: searchMode,
|
||
type: searchType,
|
||
page: 1,
|
||
page_size: 50,
|
||
};
|
||
|
||
// 添加日期范围
|
||
if (dateRange.start || dateRange.end) {
|
||
searchParams.date_range = {};
|
||
if (dateRange.start) searchParams.date_range.start = dateRange.start.replace(/-/g, '');
|
||
if (dateRange.end) searchParams.date_range.end = dateRange.end.replace(/-/g, '');
|
||
}
|
||
|
||
onSearch(searchParams);
|
||
};
|
||
|
||
const clearSearch = () => {
|
||
setSearchKeyword('');
|
||
setDateRange({ start: '', end: '' });
|
||
setSearchType('all');
|
||
setSearchMode('hybrid');
|
||
};
|
||
|
||
return (
|
||
<Card bg={cardBg} borderRadius="xl" boxShadow="xl" mb={6}>
|
||
<CardBody>
|
||
<VStack spacing={4}>
|
||
<HStack w="full" spacing={3}>
|
||
<InputGroup size="lg" flex={1}>
|
||
<InputLeftElement>
|
||
<SearchIcon color="gray.400" />
|
||
</InputLeftElement>
|
||
<Input
|
||
placeholder="搜索股票名称、代码或涨停原因..."
|
||
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>
|
||
|
||
<HStack w="full" spacing={4} align="start">
|
||
<Box flex={1}>
|
||
<Text fontSize="sm" mb={2} fontWeight="bold">搜索类型</Text>
|
||
<RadioGroup value={searchType} onChange={setSearchType}>
|
||
<HStack spacing={4}>
|
||
<Radio value="all">全部</Radio>
|
||
<Radio value="stock">股票</Radio>
|
||
<Radio value="reason">涨停原因</Radio>
|
||
</HStack>
|
||
</RadioGroup>
|
||
</Box>
|
||
|
||
<Box flex={1}>
|
||
<Text fontSize="sm" mb={2} fontWeight="bold">搜索模式</Text>
|
||
<Select
|
||
value={searchMode}
|
||
onChange={(e) => setSearchMode(e.target.value)}
|
||
bg="white"
|
||
_dark={{ bg: 'gray.700' }}
|
||
sx={{
|
||
'& option': {
|
||
bg: 'white',
|
||
color: 'gray.800',
|
||
_dark: {
|
||
bg: 'gray.700',
|
||
color: 'white'
|
||
}
|
||
}
|
||
}}
|
||
>
|
||
<option value="hybrid">智能搜索(推荐)</option>
|
||
<option value="text">精确匹配</option>
|
||
<option value="vector">语义搜索</option>
|
||
</Select>
|
||
</Box>
|
||
|
||
<Box flex={2}>
|
||
<Text fontSize="sm" mb={2} fontWeight="bold">日期范围(可选)</Text>
|
||
<HStack>
|
||
<InputGroup size="md">
|
||
<InputLeftElement>
|
||
<CalendarIcon color="gray.400" boxSize={4} />
|
||
</InputLeftElement>
|
||
<Input
|
||
type="date"
|
||
value={dateRange.start}
|
||
onChange={(e) => setDateRange({...dateRange, start: e.target.value})}
|
||
/>
|
||
</InputGroup>
|
||
<Text>至</Text>
|
||
<InputGroup size="md">
|
||
<InputLeftElement>
|
||
<CalendarIcon color="gray.400" boxSize={4} />
|
||
</InputLeftElement>
|
||
<Input
|
||
type="date"
|
||
value={dateRange.end}
|
||
onChange={(e) => setDateRange({...dateRange, end: e.target.value})}
|
||
/>
|
||
</InputGroup>
|
||
</HStack>
|
||
</Box>
|
||
</HStack>
|
||
|
||
<Alert status="info" borderRadius="md" fontSize="sm">
|
||
<AlertIcon />
|
||
<Text>
|
||
<strong>提示:</strong>搜索结果将在新窗口中显示,不会影响当前页面的数据展示。
|
||
您可以搜索不同日期范围内的涨停股票进行对比分析。
|
||
</Text>
|
||
</Alert>
|
||
</VStack>
|
||
</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>操作</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>
|
||
<HStack spacing={1}>
|
||
<Tooltip label="查看详情">
|
||
<IconButton
|
||
icon={<ViewIcon />}
|
||
size="sm"
|
||
variant="ghost"
|
||
colorScheme="blue"
|
||
onClick={() => onStockClick(stock)}
|
||
/>
|
||
</Tooltip>
|
||
<Tooltip label="查看K线">
|
||
<IconButton
|
||
icon={<ExternalLinkIcon />}
|
||
size="sm"
|
||
variant="ghost"
|
||
colorScheme="green"
|
||
/>
|
||
</Tooltip>
|
||
</HStack>
|
||
</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 }; |