update pay ui

This commit is contained in:
2025-12-14 16:43:45 +08:00
parent 8971cebaa3
commit 77f1643a58
4 changed files with 135 additions and 251 deletions

View File

@@ -11,6 +11,7 @@ const DATA_BASE_URL = '/data/zt';
const cache = { const cache = {
dates: null, dates: null,
daily: new Map(), daily: new Map(),
stocksJsonl: null, // 缓存 stocks.jsonl 数据
}; };
/** /**
@@ -203,80 +204,104 @@ const parseContinuousDays = (str) => {
}; };
/** /**
* 关键词搜索股票 * 加载 stocks.jsonl 文件
* 从缓存的数据中搜索 * JSONL 格式:每行一个 JSON 对象
*/
const loadStocksJsonl = async () => {
try {
// 使用缓存
if (cache.stocksJsonl) {
return cache.stocksJsonl;
}
const response = await fetch(`${DATA_BASE_URL}/stocks.jsonl`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const text = await response.text();
const lines = text.trim().split('\n');
const stocks = lines
.filter(line => line.trim())
.map(line => {
try {
return JSON.parse(line);
} catch {
return null;
}
})
.filter(Boolean);
// 缓存结果
cache.stocksJsonl = stocks;
return stocks;
} catch (error) {
console.error('[ztStaticService] loadStocksJsonl error:', error);
return [];
}
};
/**
* 简化版搜索股票
* 仅支持股票代码或名称的精确/部分匹配
* 使用 stocks.jsonl 作为数据源
*/ */
export const searchStocks = async (searchParams) => { export const searchStocks = async (searchParams) => {
try { try {
const { query, date, date_range, page = 1, page_size = 20 } = searchParams; const { query, page = 1, page_size = 50 } = searchParams;
if (!query || query.trim() === '') { if (!query || query.trim() === '') {
return { success: false, error: '搜索关键词不能为空' }; return { success: false, error: '请输入股票代码或名称' };
} }
const queryLower = query.toLowerCase().trim(); const queryLower = query.toLowerCase().trim();
let allStocks = [];
// 确定要搜索的日期范围 // 加载 stocks.jsonl 数据
let datesToSearch = []; const allStocks = await loadStocksJsonl();
if (date) { if (allStocks.length === 0) {
datesToSearch = [date]; return { success: false, error: '搜索数据暂未加载,请稍后重试' };
} else if (date_range?.start && date_range?.end) {
// 从缓存的日期中筛选
const datesResult = await fetchAvailableDates();
if (datesResult.success) {
datesToSearch = datesResult.events
.filter(d => d.date >= date_range.start && d.date <= date_range.end)
.map(d => d.date);
}
} else {
// 默认搜索最近 30 天
const datesResult = await fetchAvailableDates();
if (datesResult.success) {
datesToSearch = datesResult.events.slice(0, 30).map(d => d.date);
}
} }
// 从每个日期的数据中搜索 // 简单的股票代码/名称匹配
for (const d of datesToSearch) {
const result = await fetchDailyAnalysis(d);
if (result.success && result.data.stocks) {
const stocks = result.data.stocks.map(s => ({ ...s, date: d }));
allStocks = allStocks.concat(stocks);
}
}
// 关键词匹配
const results = allStocks const results = allStocks
.map(stock => { .map(stock => {
let score = 0; let score = 0;
const scode = (stock.scode || '').toLowerCase();
const sname = (stock.sname || '').toLowerCase();
// 精确匹配股票代码 // 精确匹配股票代码(最高优先级)
if (queryLower === (stock.scode || '').toLowerCase()) { if (scode === queryLower) {
score = 100; score = 100;
} }
// 精确匹配股票名称 // 精确匹配股票名称
else if (queryLower === (stock.sname || '').toLowerCase()) { else if (sname === queryLower) {
score = 90; score = 90;
} }
// 部分匹配股票名称 // 股票代码以搜索词开头
else if ((stock.sname || '').toLowerCase().includes(queryLower)) { else if (scode.startsWith(queryLower)) {
score = 80; score = 80;
} }
// 匹配板块 // 股票名称包含搜索词
else if ((stock.core_sectors || []).some(s => s.toLowerCase().includes(queryLower))) { else if (sname.includes(queryLower)) {
score = 70; score = 70;
} }
// 匹配涨停原因 // 股票代码包含搜索词
else if ((stock.brief || '').toLowerCase().includes(queryLower)) { else if (scode.includes(queryLower)) {
score = 60; score = 60;
} }
return { ...stock, _score: score }; return { ...stock, _score: score };
}) })
.filter(s => s._score > 0) .filter(s => s._score > 0)
.sort((a, b) => b._score - a._score || b.date.localeCompare(a.date)); .sort((a, b) => {
// 先按匹配度排序,再按日期降序
if (b._score !== a._score) {
return b._score - a._score;
}
return (b.date || '').localeCompare(a.date || '');
});
// 分页 // 分页
const total = results.length; const total = results.length;
@@ -291,7 +316,7 @@ export const searchStocks = async (searchParams) => {
page, page,
page_size, page_size,
total_pages: Math.ceil(total / page_size), total_pages: Math.ceil(total / page_size),
search_mode: 'keyword', search_mode: 'exact',
}, },
}; };
} catch (error) { } catch (error) {
@@ -324,6 +349,7 @@ export const fetchStocksBatchDetail = async (codes, date) => {
export const clearCache = () => { export const clearCache = () => {
cache.dates = null; cache.dates = null;
cache.daily.clear(); cache.daily.clear();
cache.stocksJsonl = null;
}; };
export default { export default {

View File

@@ -512,7 +512,7 @@ const SectorRelationMap = ({ data }) => {
}; };
// 数据分析主组件 // 数据分析主组件
export const DataAnalysis = ({ dailyData, wordCloudData }) => { export const DataAnalysis = ({ dailyData, wordCloudData, totalStocks, dateStr }) => {
const cardBg = useColorModeValue('white', 'gray.800'); const cardBg = useColorModeValue('white', 'gray.800');
const pieData = useMemo(() => { const pieData = useMemo(() => {
@@ -538,10 +538,30 @@ export const DataAnalysis = ({ dailyData, wordCloudData }) => {
return Object.values(dailyData.sector_data).flatMap(sector => sector.stocks || []); return Object.values(dailyData.sector_data).flatMap(sector => sector.stocks || []);
}, [dailyData]); }, [dailyData]);
// 格式化日期显示
const formatDate = (str) => {
if (!str || str.length !== 8) return '';
return `${str.slice(0, 4)}${parseInt(str.slice(4, 6))}${parseInt(str.slice(6, 8))}`;
};
return ( return (
<Card bg={cardBg} borderRadius="xl" boxShadow="xl"> <Card bg={cardBg} borderRadius="xl" boxShadow="xl">
<CardHeader> <CardHeader>
<HStack justify="space-between" align="center" flexWrap="wrap" gap={2}>
<Heading size="md">数据分析</Heading> <Heading size="md">数据分析</Heading>
{totalStocks !== undefined && (
<HStack spacing={3}>
{dateStr && (
<Badge colorScheme="gray" fontSize="sm" px={3} py={1}>
{formatDate(dateStr)}
</Badge>
)}
<Badge colorScheme="red" fontSize="lg" px={4} py={2} borderRadius="md">
今日涨停: {totalStocks}
</Badge>
</HStack>
)}
</HStack>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<Tabs variant="soft-rounded" colorScheme="blue" isLazy> <Tabs variant="soft-rounded" colorScheme="blue" isLazy>

View File

@@ -1,18 +1,13 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
Box,
Card, Card,
CardBody, CardBody,
VStack,
HStack, HStack,
Text, Text,
Button, Button,
Input, Input,
InputGroup, InputGroup,
InputLeftElement, InputLeftElement,
Select,
RadioGroup,
Radio,
Modal, Modal,
ModalOverlay, ModalOverlay,
ModalContent, ModalContent,
@@ -39,14 +34,11 @@ import {
AlertIcon, AlertIcon,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { formatTooltipText, getFormattedTextProps } from '../../../utils/textUtils'; import { formatTooltipText, getFormattedTextProps } from '../../../utils/textUtils';
import { SearchIcon, CalendarIcon, DownloadIcon } from '@chakra-ui/icons'; import { SearchIcon, DownloadIcon } from '@chakra-ui/icons';
// 高级搜索组件 // 简化版搜索组件 - 仅支持股票代码/名称精确匹配
export const AdvancedSearch = ({ onSearch, loading }) => { export const AdvancedSearch = ({ onSearch, loading }) => {
const [searchKeyword, setSearchKeyword] = useState(''); 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 cardBg = useColorModeValue('white', 'gray.800');
const toast = useToast(); const toast = useToast();
@@ -54,7 +46,7 @@ export const AdvancedSearch = ({ onSearch, loading }) => {
const handleSearch = () => { const handleSearch = () => {
if (!searchKeyword.trim()) { if (!searchKeyword.trim()) {
toast({ toast({
title: '请输入搜索关键词', title: '请输入股票代码或名称',
status: 'warning', status: 'warning',
duration: 2000, duration: 2000,
}); });
@@ -62,41 +54,29 @@ export const AdvancedSearch = ({ onSearch, loading }) => {
} }
const searchParams = { const searchParams = {
query: searchKeyword, query: searchKeyword.trim(),
mode: searchMode, mode: 'exact', // 固定为精确匹配
type: searchType,
page: 1, page: 1,
page_size: 50, 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); onSearch(searchParams);
}; };
const clearSearch = () => { const clearSearch = () => {
setSearchKeyword(''); setSearchKeyword('');
setDateRange({ start: '', end: '' });
setSearchType('all');
setSearchMode('hybrid');
}; };
return ( return (
<Card bg={cardBg} borderRadius="xl" boxShadow="xl" mb={6}> <Card bg={cardBg} borderRadius="xl" boxShadow="xl" mb={6}>
<CardBody> <CardBody>
<VStack spacing={4}>
<HStack w="full" spacing={3}> <HStack w="full" spacing={3}>
<InputGroup size="lg" flex={1}> <InputGroup size="lg" flex={1}>
<InputLeftElement> <InputLeftElement>
<SearchIcon color="gray.400" /> <SearchIcon color="gray.400" />
</InputLeftElement> </InputLeftElement>
<Input <Input
placeholder="搜索股票名称、代码或涨停原因..." placeholder="输入股票代码或名称搜索(如 600000 或 浦发银行)"
value={searchKeyword} value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)} onChange={(e) => setSearchKeyword(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()} onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
@@ -122,79 +102,6 @@ export const AdvancedSearch = ({ onSearch, loading }) => {
清空 清空
</Button> </Button>
</HStack> </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> </CardBody>
</Card> </Card>
); );

View File

@@ -16,11 +16,6 @@ import {
Tooltip, Tooltip,
Card, Card,
CardBody, CardBody,
Stat,
StatLabel,
StatNumber,
StatHelpText,
StatArrow,
Alert, Alert,
AlertIcon, AlertIcon,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
@@ -247,61 +242,6 @@ export default function LimitAnalyse() {
return result; return result;
}; };
// 渲染统计卡片
const StatsCards = () => (
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4} mb={6}>
<Card bg={cardBg} borderRadius="lg" boxShadow="md">
<CardBody>
<Stat>
<StatLabel fontSize="sm">今日涨停</StatLabel>
<StatNumber fontSize="2xl" color="red.500">
{dailyData?.total_stocks || 0}
</StatNumber>
<StatHelpText>
<StatArrow type="increase" />
较昨日 +23%
</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} borderRadius="lg" boxShadow="md">
<CardBody>
<Stat>
<StatLabel fontSize="sm">最热板块</StatLabel>
<StatNumber fontSize="xl" color={accentColor}>
{dailyData?.summary?.top_sector || '-'}
</StatNumber>
<StatHelpText>{dailyData?.summary?.top_sector_count || 0} </StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} borderRadius="lg" boxShadow="md">
<CardBody>
<Stat>
<StatLabel fontSize="sm">公告涨停</StatLabel>
<StatNumber fontSize="2xl" color="orange.500">
{dailyData?.summary?.announcement_stocks || 0}
</StatNumber>
<StatHelpText>重大利好</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} borderRadius="lg" boxShadow="md">
<CardBody>
<Stat>
<StatLabel fontSize="sm">早盘强势</StatLabel>
<StatNumber fontSize="2xl" color="green.500">
{dailyData?.summary?.zt_time_distribution?.morning || 0}
</StatNumber>
<StatHelpText>开盘涨停</StatHelpText>
</Stat>
</CardBody>
</Card>
</SimpleGrid>
);
const formatDisplayDate = (date) => { const formatDisplayDate = (date) => {
if (!date) return ''; if (!date) return '';
@@ -431,21 +371,10 @@ export default function LimitAnalyse() {
{/* 主内容区 */} {/* 主内容区 */}
<Container maxW="container.xl" py={8}> <Container maxW="container.xl" py={8}>
{/* 统计卡片 */} {/* 搜索框 */}
{loading ? (
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4} mb={6}>
{[...Array(4)].map((_, i) => (
<Skeleton key={i} height="100px" borderRadius="lg" />
))}
</SimpleGrid>
) : (
<StatsCards />
)}
{/* 高级搜索 */}
<AdvancedSearch onSearch={handleSearch} loading={loading} /> <AdvancedSearch onSearch={handleSearch} loading={loading} />
{/* 数据分析 - 移到板块详情上方 */} {/* 数据分析(含涨停统计) */}
{loading ? ( {loading ? (
<Skeleton height="500px" borderRadius="xl" mb={6} /> <Skeleton height="500px" borderRadius="xl" mb={6} />
) : ( ) : (
@@ -453,6 +382,8 @@ export default function LimitAnalyse() {
<DataAnalysis <DataAnalysis
dailyData={dailyData} dailyData={dailyData}
wordCloudData={wordCloudData} wordCloudData={wordCloudData}
totalStocks={dailyData?.total_stocks || 0}
dateStr={dateStr}
/> />
</Box> </Box>
)} )}