Files
vf_react/src/views/LimitAnalyse/components/SearchComponents.js
2025-11-29 09:42:41 +08:00

423 lines
19 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 {
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 };