Initial commit

This commit is contained in:
2025-10-11 11:55:25 +08:00
parent 467dad8449
commit 8107dee8d3
2879 changed files with 610575 additions and 0 deletions

View 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 };