Initial commit
This commit is contained in:
408
src/views/LimitAnalyse/components/SearchComponents.js
Normal file
408
src/views/LimitAnalyse/components/SearchComponents.js
Normal file
@@ -0,0 +1,408 @@
|
||||
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)}>
|
||||
<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 };
|
||||
Reference in New Issue
Block a user