update pay ui
This commit is contained in:
45
app.py
45
app.py
@@ -12824,6 +12824,9 @@ def get_hotspot_overview():
|
|||||||
'surge': len([a for a in alerts if a['alert_type'] == 'surge']),
|
'surge': len([a for a in alerts if a['alert_type'] == 'surge']),
|
||||||
'surge_up': len([a for a in alerts if a['alert_type'] == 'surge_up']),
|
'surge_up': len([a for a in alerts if a['alert_type'] == 'surge_up']),
|
||||||
'surge_down': len([a for a in alerts if a['alert_type'] == 'surge_down']),
|
'surge_down': len([a for a in alerts if a['alert_type'] == 'surge_down']),
|
||||||
|
'volume_surge_up': len([a for a in alerts if a['alert_type'] == 'volume_surge_up']),
|
||||||
|
'shrink_surge_up': len([a for a in alerts if a['alert_type'] == 'shrink_surge_up']),
|
||||||
|
'volume_oscillation': len([a for a in alerts if a['alert_type'] == 'volume_oscillation']),
|
||||||
'limit_up': len([a for a in alerts if a['alert_type'] == 'limit_up']),
|
'limit_up': len([a for a in alerts if a['alert_type'] == 'limit_up']),
|
||||||
'volume_spike': len([a for a in alerts if a['alert_type'] == 'volume_spike']),
|
'volume_spike': len([a for a in alerts if a['alert_type'] == 'volume_spike']),
|
||||||
'rank_jump': len([a for a in alerts if a['alert_type'] == 'rank_jump'])
|
'rank_jump': len([a for a in alerts if a['alert_type'] == 'rank_jump'])
|
||||||
@@ -12848,7 +12851,7 @@ def get_concept_stocks(concept_id):
|
|||||||
获取概念的相关股票列表(带实时涨跌幅)
|
获取概念的相关股票列表(带实时涨跌幅)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
concept_id: 概念 ID(来自 ES concept_library_v3)
|
concept_id: 概念 ID 或概念名称(支持两种方式查询)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- stocks: 股票列表 [{code, name, reason, change_pct}, ...]
|
- stocks: 股票列表 [{code, name, reason, change_pct}, ...]
|
||||||
@@ -12857,18 +12860,48 @@ def get_concept_stocks(concept_id):
|
|||||||
from elasticsearch import Elasticsearch
|
from elasticsearch import Elasticsearch
|
||||||
from clickhouse_driver import Client
|
from clickhouse_driver import Client
|
||||||
|
|
||||||
# 1. 从 ES 获取概念的股票列表
|
|
||||||
es_client = Elasticsearch(["http://222.128.1.157:19200"])
|
es_client = Elasticsearch(["http://222.128.1.157:19200"])
|
||||||
es_result = es_client.get(index='concept_library_v3', id=concept_id)
|
|
||||||
|
|
||||||
if not es_result.get('found'):
|
# 1. 尝试多种方式获取概念数据
|
||||||
|
source = None
|
||||||
|
concept_name = concept_id
|
||||||
|
|
||||||
|
# 方式1: 先尝试按 ID 查询
|
||||||
|
try:
|
||||||
|
es_result = es_client.get(index='concept_library_v3', id=concept_id)
|
||||||
|
if es_result.get('found'):
|
||||||
|
source = es_result.get('_source', {})
|
||||||
|
concept_name = source.get('concept', concept_id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 方式2: 如果按 ID 没找到,尝试按概念名称搜索
|
||||||
|
if not source:
|
||||||
|
try:
|
||||||
|
search_result = es_client.search(
|
||||||
|
index='concept_library_v3',
|
||||||
|
body={
|
||||||
|
'query': {
|
||||||
|
'term': {
|
||||||
|
'concept.keyword': concept_id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'size': 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
hits = search_result.get('hits', {}).get('hits', [])
|
||||||
|
if hits:
|
||||||
|
source = hits[0].get('_source', {})
|
||||||
|
concept_name = source.get('concept', concept_id)
|
||||||
|
except Exception as search_err:
|
||||||
|
app.logger.debug(f"ES 搜索概念失败: {search_err}")
|
||||||
|
|
||||||
|
if not source:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': f'概念 {concept_id} 不存在'
|
'error': f'概念 {concept_id} 不存在'
|
||||||
}), 404
|
}), 404
|
||||||
|
|
||||||
source = es_result.get('_source', {})
|
|
||||||
concept_name = source.get('concept', concept_id)
|
|
||||||
raw_stocks = source.get('stocks', [])
|
raw_stocks = source.get('stocks', [])
|
||||||
|
|
||||||
if not raw_stocks:
|
if not raw_stocks:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* 异动统计摘要组件
|
* 异动统计摘要组件 - 科技感设计
|
||||||
* 展示指数统计和异动类型统计
|
* 展示指数统计和异动类型统计
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -17,84 +17,250 @@ import {
|
|||||||
StatArrow,
|
StatArrow,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
|
Tooltip,
|
||||||
|
Flex,
|
||||||
|
keyframes,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { FaBolt, FaArrowDown, FaRocket, FaChartLine, FaFire, FaVolumeUp } from 'react-icons/fa';
|
import {
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
Zap,
|
||||||
|
Activity,
|
||||||
|
Flame,
|
||||||
|
BarChart3,
|
||||||
|
Target,
|
||||||
|
Waves,
|
||||||
|
Rocket,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowDown,
|
||||||
|
Minus,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { ALERT_TYPE_CONFIG, getAlertTypeLabel } from '../utils/chartHelpers';
|
||||||
|
|
||||||
|
// 动画效果
|
||||||
|
const pulseAnimation = keyframes`
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.6; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const glowAnimation = keyframes`
|
||||||
|
0%, 100% { box-shadow: 0 0 5px currentColor; }
|
||||||
|
50% { box-shadow: 0 0 15px currentColor; }
|
||||||
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异动类型徽章
|
* 获取异动类型对应的图标
|
||||||
|
*/
|
||||||
|
const getAlertIcon = (alertType) => {
|
||||||
|
const iconMap = {
|
||||||
|
surge_up: TrendingUp,
|
||||||
|
surge: Zap,
|
||||||
|
surge_down: TrendingDown,
|
||||||
|
volume_surge_up: Activity,
|
||||||
|
shrink_surge_up: Rocket,
|
||||||
|
volume_oscillation: Waves,
|
||||||
|
limit_up: Flame,
|
||||||
|
rank_jump: Target,
|
||||||
|
volume_spike: BarChart3,
|
||||||
|
};
|
||||||
|
return iconMap[alertType] || Zap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 科技感异动类型徽章
|
||||||
*/
|
*/
|
||||||
const AlertTypeBadge = ({ type, count }) => {
|
const AlertTypeBadge = ({ type, count }) => {
|
||||||
const config = {
|
const config = ALERT_TYPE_CONFIG[type] || { label: type, color: '#8c8c8c', gradient: ['#8c8c8c', '#a6a6a6'] };
|
||||||
surge: { label: '急涨', color: 'red', icon: FaBolt },
|
const AlertIcon = getAlertIcon(type);
|
||||||
surge_up: { label: '暴涨', color: 'red', icon: FaBolt },
|
|
||||||
surge_down: { label: '暴跌', color: 'green', icon: FaArrowDown },
|
|
||||||
limit_up: { label: '涨停', color: 'orange', icon: FaRocket },
|
|
||||||
rank_jump: { label: '排名跃升', color: 'blue', icon: FaChartLine },
|
|
||||||
volume_spike: { label: '放量', color: 'purple', icon: FaVolumeUp },
|
|
||||||
};
|
|
||||||
|
|
||||||
const cfg = config[type] || { label: type, color: 'gray', icon: FaFire };
|
const bgOpacity = useColorModeValue('15', '20');
|
||||||
|
const hoverBg = useColorModeValue(`${config.color}25`, `${config.color}30`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge colorScheme={cfg.color} variant="subtle" px={2} py={1} borderRadius="md">
|
<Tooltip
|
||||||
<HStack spacing={1}>
|
label={config.description || `${getAlertTypeLabel(type)} ${count} 次`}
|
||||||
<Icon as={cfg.icon} boxSize={3} />
|
hasArrow
|
||||||
<Text>{cfg.label}</Text>
|
placement="top"
|
||||||
<Text fontWeight="bold">{count}</Text>
|
>
|
||||||
</HStack>
|
<Badge
|
||||||
</Badge>
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
gap={1.5}
|
||||||
|
px={2.5}
|
||||||
|
py={1.5}
|
||||||
|
borderRadius="lg"
|
||||||
|
bg={`${config.color}${bgOpacity}`}
|
||||||
|
color={config.color}
|
||||||
|
cursor="pointer"
|
||||||
|
transition="all 0.2s"
|
||||||
|
_hover={{
|
||||||
|
bg: hoverBg,
|
||||||
|
transform: 'translateY(-1px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon as={AlertIcon} boxSize={3.5} />
|
||||||
|
<Text fontSize="xs" fontWeight="medium">{getAlertTypeLabel(type)}</Text>
|
||||||
|
<Box
|
||||||
|
px={1.5}
|
||||||
|
py={0.5}
|
||||||
|
bg={config.color}
|
||||||
|
color="white"
|
||||||
|
borderRadius="md"
|
||||||
|
fontSize="10px"
|
||||||
|
fontWeight="bold"
|
||||||
|
minW="18px"
|
||||||
|
textAlign="center"
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</Box>
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 指数统计卡片
|
* 迷你统计卡片
|
||||||
|
*/
|
||||||
|
const MiniStatCard = ({ label, value, change, isUp, color, tooltip }) => {
|
||||||
|
const cardBg = useColorModeValue('white', '#0d0d0d');
|
||||||
|
const borderColor = useColorModeValue('gray.200', '#2d2d2d');
|
||||||
|
const labelColor = useColorModeValue('gray.500', 'gray.400');
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<Box
|
||||||
|
bg={cardBg}
|
||||||
|
borderRadius="xl"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
p={3}
|
||||||
|
minW="100px"
|
||||||
|
transition="all 0.2s"
|
||||||
|
_hover={{
|
||||||
|
borderColor: color,
|
||||||
|
boxShadow: `0 2px 12px ${color}20`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fontSize="xs" color={labelColor} mb={1}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<HStack spacing={1} align="baseline">
|
||||||
|
<Text fontSize="lg" fontWeight="bold" color={color}>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
{change !== undefined && (
|
||||||
|
<HStack spacing={0.5} fontSize="xs" color={isUp ? '#ff4d4f' : '#52c41a'}>
|
||||||
|
<Icon as={isUp ? ArrowUp : ArrowDown} boxSize={3} />
|
||||||
|
<Text>{Math.abs(change).toFixed(2)}%</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tooltip) {
|
||||||
|
return <Tooltip label={tooltip} hasArrow>{content}</Tooltip>;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指数统计卡片 - 科技感设计
|
||||||
*/
|
*/
|
||||||
const IndexStatCard = ({ indexData }) => {
|
const IndexStatCard = ({ indexData }) => {
|
||||||
const cardBg = useColorModeValue('white', '#1a1a1a');
|
const cardBg = useColorModeValue('white', '#0d0d0d');
|
||||||
const borderColor = useColorModeValue('gray.200', '#333');
|
const borderColor = useColorModeValue('gray.200', '#2d2d2d');
|
||||||
const subTextColor = useColorModeValue('gray.600', 'gray.400');
|
const labelColor = useColorModeValue('gray.500', 'gray.400');
|
||||||
|
const gradientBg = useColorModeValue(
|
||||||
|
'linear(to-br, white, gray.50)',
|
||||||
|
'linear(to-br, #0d0d0d, #1a1a1a)'
|
||||||
|
);
|
||||||
|
|
||||||
if (!indexData) return null;
|
if (!indexData) return null;
|
||||||
|
|
||||||
const changePct = indexData.change_pct || 0;
|
const changePct = indexData.change_pct || 0;
|
||||||
const isUp = changePct >= 0;
|
const isUp = changePct >= 0;
|
||||||
|
const mainColor = isUp ? '#ff4d4f' : '#52c41a';
|
||||||
|
|
||||||
|
// 计算振幅
|
||||||
|
const amplitude = indexData.high && indexData.low && indexData.prev_close
|
||||||
|
? ((indexData.high - indexData.low) / indexData.prev_close * 100).toFixed(2)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4}>
|
<Box
|
||||||
<Stat size="sm">
|
bg={cardBg}
|
||||||
<StatLabel color={subTextColor}>{indexData.name || '上证指数'}</StatLabel>
|
bgGradient={gradientBg}
|
||||||
<StatNumber fontSize="xl" color={isUp ? 'red.500' : 'green.500'}>
|
borderRadius="xl"
|
||||||
{indexData.latest_price?.toFixed(2) || '-'}
|
borderWidth="1px"
|
||||||
</StatNumber>
|
borderColor={borderColor}
|
||||||
<StatHelpText mb={0}>
|
p={4}
|
||||||
<StatArrow type={isUp ? 'increase' : 'decrease'} />
|
position="relative"
|
||||||
{changePct?.toFixed(2)}%
|
overflow="hidden"
|
||||||
</StatHelpText>
|
>
|
||||||
</Stat>
|
{/* 背景装饰 */}
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top="-20px"
|
||||||
|
right="-20px"
|
||||||
|
w="100px"
|
||||||
|
h="100px"
|
||||||
|
borderRadius="full"
|
||||||
|
bg={`${mainColor}08`}
|
||||||
|
filter="blur(30px)"
|
||||||
|
/>
|
||||||
|
|
||||||
<Stat size="sm">
|
<Flex justify="space-between" align="flex-start" flexWrap="wrap" gap={4}>
|
||||||
<StatLabel color={subTextColor}>最高</StatLabel>
|
{/* 主指数信息 */}
|
||||||
<StatNumber fontSize="xl" color="red.500">
|
<VStack align="flex-start" spacing={1}>
|
||||||
{indexData.high?.toFixed(2) || '-'}
|
<Text fontSize="sm" color={labelColor} fontWeight="medium">
|
||||||
</StatNumber>
|
{indexData.name || '上证指数'}
|
||||||
</Stat>
|
</Text>
|
||||||
|
<HStack spacing={2} align="baseline">
|
||||||
|
<Text fontSize="2xl" fontWeight="bold" color={mainColor}>
|
||||||
|
{indexData.latest_price?.toFixed(2) || '-'}
|
||||||
|
</Text>
|
||||||
|
<HStack
|
||||||
|
spacing={1}
|
||||||
|
px={2}
|
||||||
|
py={0.5}
|
||||||
|
borderRadius="full"
|
||||||
|
bg={`${mainColor}15`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
as={isUp ? TrendingUp : TrendingDown}
|
||||||
|
boxSize={3}
|
||||||
|
color={mainColor}
|
||||||
|
/>
|
||||||
|
<Text fontSize="sm" fontWeight="bold" color={mainColor}>
|
||||||
|
{isUp ? '+' : ''}{changePct?.toFixed(2)}%
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
<Stat size="sm">
|
{/* 详细数据 */}
|
||||||
<StatLabel color={subTextColor}>最低</StatLabel>
|
<SimpleGrid columns={3} spacing={3}>
|
||||||
<StatNumber fontSize="xl" color="green.500">
|
<VStack spacing={0} align="flex-start">
|
||||||
{indexData.low?.toFixed(2) || '-'}
|
<Text fontSize="xs" color={labelColor}>最高</Text>
|
||||||
</StatNumber>
|
<Text fontSize="sm" fontWeight="bold" color="#ff4d4f">
|
||||||
</Stat>
|
{indexData.high?.toFixed(2) || '-'}
|
||||||
|
</Text>
|
||||||
<Stat size="sm">
|
</VStack>
|
||||||
<StatLabel color={subTextColor}>振幅</StatLabel>
|
<VStack spacing={0} align="flex-start">
|
||||||
<StatNumber fontSize="xl" color="purple.500">
|
<Text fontSize="xs" color={labelColor}>最低</Text>
|
||||||
{indexData.high && indexData.low && indexData.prev_close
|
<Text fontSize="sm" fontWeight="bold" color="#52c41a">
|
||||||
? (((indexData.high - indexData.low) / indexData.prev_close) * 100).toFixed(2) + '%'
|
{indexData.low?.toFixed(2) || '-'}
|
||||||
: '-'}
|
</Text>
|
||||||
</StatNumber>
|
</VStack>
|
||||||
</Stat>
|
<VStack spacing={0} align="flex-start">
|
||||||
</SimpleGrid>
|
<Text fontSize="xs" color={labelColor}>振幅</Text>
|
||||||
|
<Text fontSize="sm" fontWeight="bold" color="#722ed1">
|
||||||
|
{amplitude ? `${amplitude}%` : '-'}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,8 +272,9 @@ const IndexStatCard = ({ indexData }) => {
|
|||||||
* @param {Object} props.alertSummary - 异动类型统计
|
* @param {Object} props.alertSummary - 异动类型统计
|
||||||
*/
|
*/
|
||||||
const AlertSummary = ({ indexData, alerts = [], alertSummary = {} }) => {
|
const AlertSummary = ({ indexData, alerts = [], alertSummary = {} }) => {
|
||||||
const cardBg = useColorModeValue('white', '#1a1a1a');
|
const cardBg = useColorModeValue('white', '#0d0d0d');
|
||||||
const borderColor = useColorModeValue('gray.200', '#333');
|
const borderColor = useColorModeValue('gray.200', '#2d2d2d');
|
||||||
|
const labelColor = useColorModeValue('gray.500', 'gray.400');
|
||||||
|
|
||||||
// 如果没有 alertSummary,从 alerts 中统计
|
// 如果没有 alertSummary,从 alerts 中统计
|
||||||
const summary = alertSummary && Object.keys(alertSummary).length > 0
|
const summary = alertSummary && Object.keys(alertSummary).length > 0
|
||||||
@@ -120,6 +287,22 @@ const AlertSummary = ({ indexData, alerts = [], alertSummary = {} }) => {
|
|||||||
|
|
||||||
const totalAlerts = alerts.length;
|
const totalAlerts = alerts.length;
|
||||||
|
|
||||||
|
// 按优先级排序的异动类型
|
||||||
|
const sortedTypes = [
|
||||||
|
'surge_up',
|
||||||
|
'volume_surge_up',
|
||||||
|
'shrink_surge_up',
|
||||||
|
'surge',
|
||||||
|
'surge_down',
|
||||||
|
'volume_oscillation',
|
||||||
|
'limit_up',
|
||||||
|
'rank_jump',
|
||||||
|
'volume_spike',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 获取有数据的类型
|
||||||
|
const activeTypes = sortedTypes.filter(type => (summary[type] || 0) > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack spacing={4} align="stretch">
|
<VStack spacing={4} align="stretch">
|
||||||
{/* 指数统计 */}
|
{/* 指数统计 */}
|
||||||
@@ -127,18 +310,67 @@ const AlertSummary = ({ indexData, alerts = [], alertSummary = {} }) => {
|
|||||||
|
|
||||||
{/* 异动统计 */}
|
{/* 异动统计 */}
|
||||||
{totalAlerts > 0 && (
|
{totalAlerts > 0 && (
|
||||||
<HStack spacing={2} flexWrap="wrap">
|
<Box
|
||||||
<Text fontSize="sm" color="gray.500" mr={2}>
|
bg={cardBg}
|
||||||
异动 {totalAlerts} 次:
|
borderRadius="xl"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
p={4}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center" mb={3}>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Icon as={Zap} boxSize={4} color="#faad14" />
|
||||||
|
<Text fontSize="sm" fontWeight="medium">
|
||||||
|
今日异动
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<Badge
|
||||||
|
px={2}
|
||||||
|
py={1}
|
||||||
|
borderRadius="full"
|
||||||
|
bg="#faad1415"
|
||||||
|
color="#faad14"
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{totalAlerts} 次
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap={2} flexWrap="wrap">
|
||||||
|
{activeTypes.map(type => (
|
||||||
|
<AlertTypeBadge
|
||||||
|
key={type}
|
||||||
|
type={type}
|
||||||
|
count={summary[type]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 如果有其他未分类的类型 */}
|
||||||
|
{Object.entries(summary)
|
||||||
|
.filter(([type, count]) => !sortedTypes.includes(type) && count > 0)
|
||||||
|
.map(([type, count]) => (
|
||||||
|
<AlertTypeBadge key={type} type={type} count={count} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 无异动提示 */}
|
||||||
|
{totalAlerts === 0 && (
|
||||||
|
<Box
|
||||||
|
bg={cardBg}
|
||||||
|
borderRadius="xl"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
p={4}
|
||||||
|
textAlign="center"
|
||||||
|
>
|
||||||
|
<Icon as={Minus} boxSize={5} color={labelColor} mb={2} />
|
||||||
|
<Text fontSize="sm" color={labelColor}>
|
||||||
|
暂无异动数据
|
||||||
</Text>
|
</Text>
|
||||||
{(summary.surge_up > 0 || summary.surge > 0) && (
|
</Box>
|
||||||
<AlertTypeBadge type="surge_up" count={(summary.surge_up || 0) + (summary.surge || 0)} />
|
|
||||||
)}
|
|
||||||
{summary.surge_down > 0 && <AlertTypeBadge type="surge_down" count={summary.surge_down} />}
|
|
||||||
{summary.limit_up > 0 && <AlertTypeBadge type="limit_up" count={summary.limit_up} />}
|
|
||||||
{summary.volume_spike > 0 && <AlertTypeBadge type="volume_spike" count={summary.volume_spike} />}
|
|
||||||
{summary.rank_jump > 0 && <AlertTypeBadge type="rank_jump" count={summary.rank_jump} />}
|
|
||||||
</HStack>
|
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* 概念异动列表组件 - V2
|
* 概念异动列表组件 - V2 科技感设计
|
||||||
* 展示当日的概念异动记录,点击可展开显示相关股票
|
* 展示当日的概念异动记录,点击可展开显示相关股票
|
||||||
*/
|
*/
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Collapse,
|
Collapse,
|
||||||
Spinner,
|
Spinner,
|
||||||
Progress,
|
|
||||||
Table,
|
Table,
|
||||||
Thead,
|
Thead,
|
||||||
Tbody,
|
Tbody,
|
||||||
@@ -28,28 +27,216 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverBody,
|
PopoverBody,
|
||||||
Portal,
|
Portal,
|
||||||
|
chakra,
|
||||||
|
keyframes,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { FaArrowUp, FaArrowDown, FaFire, FaChevronDown, FaChevronRight } from 'react-icons/fa';
|
import {
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
Zap,
|
||||||
|
Activity,
|
||||||
|
Flame,
|
||||||
|
BarChart3,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
CircleHelp,
|
||||||
|
Sparkles,
|
||||||
|
Target,
|
||||||
|
Gauge,
|
||||||
|
Waves,
|
||||||
|
Rocket,
|
||||||
|
} from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { getAlertTypeLabel, formatScore, getScoreColor } from '../utils/chartHelpers';
|
import {
|
||||||
|
ALERT_TYPE_CONFIG,
|
||||||
|
METRIC_CONFIG,
|
||||||
|
TRIGGERED_RULES_CONFIG,
|
||||||
|
getAlertTypeLabel,
|
||||||
|
getAlertTypeDescription,
|
||||||
|
getAlertTypeColor,
|
||||||
|
formatScore,
|
||||||
|
getScoreColor,
|
||||||
|
getScoreLevel,
|
||||||
|
formatMetric,
|
||||||
|
} from '../utils/chartHelpers';
|
||||||
import MiniTimelineChart from '@components/Charts/Stock/MiniTimelineChart';
|
import MiniTimelineChart from '@components/Charts/Stock/MiniTimelineChart';
|
||||||
|
|
||||||
|
// 动画效果
|
||||||
|
const pulseGlow = keyframes`
|
||||||
|
0%, 100% { box-shadow: 0 0 5px currentColor; }
|
||||||
|
50% { box-shadow: 0 0 15px currentColor, 0 0 25px currentColor; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const shimmer = keyframes`
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 紧凑型异动卡片
|
* 获取异动类型对应的图标
|
||||||
|
*/
|
||||||
|
const getAlertIcon = (alertType) => {
|
||||||
|
const iconMap = {
|
||||||
|
surge_up: TrendingUp,
|
||||||
|
surge: Zap,
|
||||||
|
surge_down: TrendingDown,
|
||||||
|
volume_surge_up: Activity,
|
||||||
|
shrink_surge_up: Rocket,
|
||||||
|
volume_oscillation: Waves,
|
||||||
|
limit_up: Flame,
|
||||||
|
rank_jump: Target,
|
||||||
|
volume_spike: BarChart3,
|
||||||
|
};
|
||||||
|
return iconMap[alertType] || Zap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指标提示组件 - 带详细说明
|
||||||
|
*/
|
||||||
|
const MetricTooltip = ({ metricKey, children }) => {
|
||||||
|
const config = METRIC_CONFIG[metricKey];
|
||||||
|
if (!config) return children;
|
||||||
|
|
||||||
|
const tooltipBg = useColorModeValue('gray.800', 'gray.700');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
label={
|
||||||
|
<VStack align="start" spacing={1} p={1}>
|
||||||
|
<HStack>
|
||||||
|
<Icon as={CircleHelp} boxSize={3} />
|
||||||
|
<Text fontWeight="bold">{config.label}</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text fontSize="xs" opacity={0.9}>{config.tooltip}</Text>
|
||||||
|
</VStack>
|
||||||
|
}
|
||||||
|
bg={tooltipBg}
|
||||||
|
color="white"
|
||||||
|
px={3}
|
||||||
|
py={2}
|
||||||
|
borderRadius="md"
|
||||||
|
maxW="280px"
|
||||||
|
hasArrow
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迷你进度条组件
|
||||||
|
*/
|
||||||
|
const MiniProgressBar = ({ value, maxValue = 100, color, width = '40px', showGlow = false }) => {
|
||||||
|
const bgColor = useColorModeValue('gray.200', 'gray.700');
|
||||||
|
const percent = Math.min((value / maxValue) * 100, 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
w={width}
|
||||||
|
h="4px"
|
||||||
|
bg={bgColor}
|
||||||
|
borderRadius="full"
|
||||||
|
overflow="hidden"
|
||||||
|
position="relative"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
h="100%"
|
||||||
|
w={`${percent}%`}
|
||||||
|
bg={color}
|
||||||
|
borderRadius="full"
|
||||||
|
transition="width 0.3s ease"
|
||||||
|
{...(showGlow && {
|
||||||
|
boxShadow: `0 0 6px ${color}`,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Z-Score 双向进度条组件
|
||||||
|
*/
|
||||||
|
const ZScoreBar = ({ value, color }) => {
|
||||||
|
const bgColor = useColorModeValue('gray.200', 'gray.700');
|
||||||
|
const absValue = Math.abs(value || 0);
|
||||||
|
const percent = Math.min(absValue / 4 * 50, 50);
|
||||||
|
const isPositive = (value || 0) >= 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box w="50px" h="4px" bg={bgColor} borderRadius="full" position="relative">
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
h="100%"
|
||||||
|
w={`${percent}%`}
|
||||||
|
bg={color}
|
||||||
|
borderRadius="full"
|
||||||
|
transition="all 0.3s ease"
|
||||||
|
{...(isPositive ? { left: '50%' } : { right: '50%' })}
|
||||||
|
boxShadow={absValue > 2 ? `0 0 6px ${color}` : 'none'}
|
||||||
|
/>
|
||||||
|
{/* 中心线 */}
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
left="50%"
|
||||||
|
top="-1px"
|
||||||
|
transform="translateX(-50%)"
|
||||||
|
w="2px"
|
||||||
|
h="6px"
|
||||||
|
bg={useColorModeValue('gray.400', 'gray.500')}
|
||||||
|
borderRadius="full"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发规则标签组件
|
||||||
|
*/
|
||||||
|
const TriggeredRuleBadge = ({ rule }) => {
|
||||||
|
const config = TRIGGERED_RULES_CONFIG[rule];
|
||||||
|
if (!config) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={config.description} hasArrow placement="top">
|
||||||
|
<Badge
|
||||||
|
size="xs"
|
||||||
|
bg={`${config.color}20`}
|
||||||
|
color={config.color}
|
||||||
|
borderRadius="full"
|
||||||
|
px={2}
|
||||||
|
py={0.5}
|
||||||
|
fontSize="10px"
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
{config.label}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 科技感异动卡片
|
||||||
*/
|
*/
|
||||||
const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const bgColor = useColorModeValue('white', '#1a1a1a');
|
|
||||||
const hoverBg = useColorModeValue('gray.50', '#252525');
|
|
||||||
const borderColor = useColorModeValue('gray.200', '#333');
|
|
||||||
const expandedBg = useColorModeValue('purple.50', '#1e1e2e');
|
|
||||||
const tableBg = useColorModeValue('gray.50', '#151520');
|
|
||||||
const popoverBg = useColorModeValue('white', '#1a1a1a');
|
|
||||||
|
|
||||||
|
// 颜色主题
|
||||||
|
const cardBg = useColorModeValue('white', '#0d0d0d');
|
||||||
|
const hoverBg = useColorModeValue('gray.50', '#1a1a1a');
|
||||||
|
const borderColor = useColorModeValue('gray.200', '#2d2d2d');
|
||||||
|
const expandedBg = useColorModeValue('gray.50', '#111111');
|
||||||
|
const tableBg = useColorModeValue('gray.50', '#0a0a0a');
|
||||||
|
const popoverBg = useColorModeValue('white', '#1a1a1a');
|
||||||
|
const textColor = useColorModeValue('gray.800', 'white');
|
||||||
|
const subTextColor = useColorModeValue('gray.500', 'gray.400');
|
||||||
|
|
||||||
|
const alertConfig = ALERT_TYPE_CONFIG[alert.alert_type] || ALERT_TYPE_CONFIG.surge;
|
||||||
const isUp = alert.alert_type !== 'surge_down';
|
const isUp = alert.alert_type !== 'surge_down';
|
||||||
const typeColor = isUp ? 'red' : 'green';
|
const AlertIcon = getAlertIcon(alert.alert_type);
|
||||||
const isV2 = alert.is_v2;
|
const scoreLevel = getScoreLevel(alert.final_score);
|
||||||
|
|
||||||
// 点击股票跳转
|
// 点击股票跳转
|
||||||
const handleStockClick = (e, stockCode) => {
|
const handleStockClick = (e, stockCode) => {
|
||||||
@@ -59,122 +246,280 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
bg={isExpanded ? expandedBg : bgColor}
|
bg={cardBg}
|
||||||
borderRadius="lg"
|
borderRadius="xl"
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor={isExpanded ? 'purple.400' : borderColor}
|
borderColor={isExpanded ? alertConfig.color : borderColor}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
transition="all 0.2s"
|
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||||
_hover={{ borderColor: 'purple.300' }}
|
_hover={{
|
||||||
|
borderColor: alertConfig.color,
|
||||||
|
transform: 'translateY(-1px)',
|
||||||
|
boxShadow: `0 4px 20px ${alertConfig.color}15`,
|
||||||
|
}}
|
||||||
|
position="relative"
|
||||||
>
|
>
|
||||||
|
{/* 顶部渐变装饰条 */}
|
||||||
|
<Box
|
||||||
|
h="3px"
|
||||||
|
bgGradient={`linear(to-r, ${alertConfig.gradient[0]}, ${alertConfig.gradient[1]})`}
|
||||||
|
opacity={isExpanded ? 1 : 0.6}
|
||||||
|
transition="opacity 0.3s"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 主卡片 - 点击展开 */}
|
{/* 主卡片 - 点击展开 */}
|
||||||
<Box
|
<Box
|
||||||
p={3}
|
p={3}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
_hover={{ bg: hoverBg }}
|
_hover={{ bg: hoverBg }}
|
||||||
|
transition="background 0.2s"
|
||||||
>
|
>
|
||||||
<Flex justify="space-between" align="center">
|
{/* 第一行:类型图标 + 名称 + 分数 */}
|
||||||
{/* 左侧:名称 + 类型 */}
|
<Flex justify="space-between" align="center" mb={2}>
|
||||||
<HStack spacing={2} flex={1} minW={0}>
|
<HStack spacing={2} flex={1} minW={0}>
|
||||||
|
{/* 展开箭头 */}
|
||||||
<Icon
|
<Icon
|
||||||
as={isExpanded ? FaChevronDown : FaChevronRight}
|
as={isExpanded ? ChevronDown : ChevronRight}
|
||||||
color="gray.400"
|
color={subTextColor}
|
||||||
boxSize={3}
|
boxSize={4}
|
||||||
|
transition="transform 0.2s"
|
||||||
/>
|
/>
|
||||||
<Icon
|
|
||||||
as={isUp ? FaArrowUp : FaArrowDown}
|
{/* 类型图标 - 带发光效果 */}
|
||||||
color={`${typeColor}.500`}
|
<Box
|
||||||
boxSize={3}
|
p={1.5}
|
||||||
/>
|
borderRadius="lg"
|
||||||
<Text fontWeight="bold" fontSize="sm" noOfLines={1} flex={1}>
|
bg={`${alertConfig.color}15`}
|
||||||
|
color={alertConfig.color}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Icon as={AlertIcon} boxSize={4} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 概念名称 */}
|
||||||
|
<Text
|
||||||
|
fontWeight="bold"
|
||||||
|
fontSize="sm"
|
||||||
|
color={textColor}
|
||||||
|
noOfLines={1}
|
||||||
|
flex={1}
|
||||||
|
>
|
||||||
{alert.concept_name}
|
{alert.concept_name}
|
||||||
</Text>
|
</Text>
|
||||||
{isV2 && (
|
|
||||||
<Badge colorScheme="purple" size="xs" variant="solid" fontSize="9px" px={1}>
|
{/* V2 标记 */}
|
||||||
|
{alert.is_v2 && (
|
||||||
|
<Badge
|
||||||
|
bg="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
|
||||||
|
color="white"
|
||||||
|
fontSize="9px"
|
||||||
|
px={1.5}
|
||||||
|
py={0.5}
|
||||||
|
borderRadius="md"
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
V2
|
V2
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* 右侧:分数 */}
|
{/* 综合评分 */}
|
||||||
<Badge
|
<MetricTooltip metricKey="final_score">
|
||||||
px={2}
|
<HStack
|
||||||
py={0.5}
|
spacing={1}
|
||||||
borderRadius="full"
|
px={2.5}
|
||||||
bg={getScoreColor(alert.final_score)}
|
py={1}
|
||||||
color="white"
|
borderRadius="full"
|
||||||
fontSize="xs"
|
bg={`${getScoreColor(alert.final_score)}15`}
|
||||||
fontWeight="bold"
|
cursor="help"
|
||||||
ml={2}
|
>
|
||||||
>
|
<Icon as={Gauge} boxSize={3} color={getScoreColor(alert.final_score)} />
|
||||||
{formatScore(alert.final_score)}分
|
<Text
|
||||||
</Badge>
|
color={getScoreColor(alert.final_score)}
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{formatScore(alert.final_score)}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color={subTextColor}>分</Text>
|
||||||
|
</HStack>
|
||||||
|
</MetricTooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 第二行:时间 + 关键指标 */}
|
{/* 第二行:时间 + 类型标签 + 确认率 */}
|
||||||
<Flex mt={2} justify="space-between" align="center" fontSize="xs">
|
<Flex justify="space-between" align="center" mb={2}>
|
||||||
<HStack spacing={2} color="gray.500">
|
<HStack spacing={2}>
|
||||||
<Text>{alert.time}</Text>
|
{/* 时间 */}
|
||||||
<Badge colorScheme={typeColor} size="sm" variant="subtle">
|
<Text fontSize="xs" color={subTextColor} fontFamily="mono">
|
||||||
{getAlertTypeLabel(alert.alert_type)}
|
{alert.time}
|
||||||
</Badge>
|
</Text>
|
||||||
{/* 确认率 */}
|
|
||||||
{isV2 && alert.confirm_ratio != null && (
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Box w="30px" h="4px" bg="gray.200" borderRadius="full" overflow="hidden">
|
|
||||||
<Box
|
|
||||||
w={`${(alert.confirm_ratio || 0) * 100}%`}
|
|
||||||
h="100%"
|
|
||||||
bg={(alert.confirm_ratio || 0) >= 0.8 ? 'green.500' : 'orange.500'}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Text>{Math.round((alert.confirm_ratio || 0) * 100)}%</Text>
|
|
||||||
</HStack>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
{/* Alpha + Z-Score 简化显示 */}
|
{/* 异动类型标签 */}
|
||||||
<HStack spacing={3}>
|
<Tooltip label={getAlertTypeDescription(alert.alert_type)} hasArrow>
|
||||||
{alert.alpha != null && (
|
<Badge
|
||||||
<Text color={(alert.alpha || 0) >= 0 ? 'red.500' : 'green.500'} fontWeight="medium">
|
bg={`${alertConfig.color}15`}
|
||||||
α {(alert.alpha || 0) >= 0 ? '+' : ''}{(alert.alpha || 0).toFixed(2)}%
|
color={alertConfig.color}
|
||||||
</Text>
|
fontSize="xs"
|
||||||
)}
|
px={2}
|
||||||
{isV2 && alert.alpha_zscore != null && (
|
py={0.5}
|
||||||
<Tooltip label={`Alpha Z-Score: ${(alert.alpha_zscore || 0).toFixed(2)}σ`}>
|
borderRadius="md"
|
||||||
<HStack spacing={0.5}>
|
fontWeight="medium"
|
||||||
<Box
|
cursor="help"
|
||||||
w="24px"
|
>
|
||||||
h="4px"
|
{getAlertTypeLabel(alert.alert_type)}
|
||||||
bg="gray.200"
|
</Badge>
|
||||||
borderRadius="full"
|
</Tooltip>
|
||||||
overflow="hidden"
|
|
||||||
position="relative"
|
{/* 确认率 */}
|
||||||
|
{alert.is_v2 && alert.confirm_ratio != null && (
|
||||||
|
<MetricTooltip metricKey="confirm_ratio">
|
||||||
|
<HStack spacing={1} cursor="help">
|
||||||
|
<MiniProgressBar
|
||||||
|
value={alert.confirm_ratio * 100}
|
||||||
|
color={
|
||||||
|
alert.confirm_ratio >= 0.8 ? '#52c41a' :
|
||||||
|
alert.confirm_ratio >= 0.6 ? '#faad14' : '#ff4d4f'
|
||||||
|
}
|
||||||
|
width="35px"
|
||||||
|
showGlow={alert.confirm_ratio >= 0.8}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
color={
|
||||||
|
alert.confirm_ratio >= 0.8 ? '#52c41a' :
|
||||||
|
alert.confirm_ratio >= 0.6 ? '#faad14' : '#ff4d4f'
|
||||||
|
}
|
||||||
|
fontWeight="medium"
|
||||||
>
|
>
|
||||||
<Box
|
{Math.round(alert.confirm_ratio * 100)}%
|
||||||
position="absolute"
|
|
||||||
left={(alert.alpha_zscore || 0) >= 0 ? '50%' : undefined}
|
|
||||||
right={(alert.alpha_zscore || 0) < 0 ? '50%' : undefined}
|
|
||||||
w={`${Math.min(Math.abs(alert.alpha_zscore || 0) / 5 * 50, 50)}%`}
|
|
||||||
h="100%"
|
|
||||||
bg={(alert.alpha_zscore || 0) >= 0 ? 'red.500' : 'green.500'}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Text color={(alert.alpha_zscore || 0) >= 0 ? 'red.400' : 'green.400'}>
|
|
||||||
{(alert.alpha_zscore || 0) >= 0 ? '+' : ''}{(alert.alpha_zscore || 0).toFixed(1)}σ
|
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Tooltip>
|
</MetricTooltip>
|
||||||
)}
|
|
||||||
{(alert.limit_up_ratio || 0) > 0.05 && (
|
|
||||||
<HStack spacing={0.5} color="orange.500">
|
|
||||||
<Icon as={FaFire} boxSize={3} />
|
|
||||||
<Text>{Math.round((alert.limit_up_ratio || 0) * 100)}%</Text>
|
|
||||||
</HStack>
|
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
|
{/* 触发原因简述 */}
|
||||||
|
{alert.trigger_reason && (
|
||||||
|
<Tooltip label={alert.trigger_reason} hasArrow placement="left">
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
color={subTextColor}
|
||||||
|
noOfLines={1}
|
||||||
|
maxW="120px"
|
||||||
|
cursor="help"
|
||||||
|
>
|
||||||
|
{alert.trigger_reason}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
{/* 第三行:核心指标 */}
|
||||||
|
<Flex justify="space-between" align="center" flexWrap="wrap" gap={2}>
|
||||||
|
{/* Alpha 超额收益 */}
|
||||||
|
{alert.alpha != null && (
|
||||||
|
<MetricTooltip metricKey="alpha">
|
||||||
|
<HStack spacing={1} cursor="help">
|
||||||
|
<Text fontSize="xs" color={subTextColor}>Alpha</Text>
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={alert.alpha >= 0 ? '#ff4d4f' : '#52c41a'}
|
||||||
|
>
|
||||||
|
{alert.alpha >= 0 ? '+' : ''}{alert.alpha.toFixed(2)}%
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</MetricTooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Alpha Z-Score */}
|
||||||
|
{alert.is_v2 && alert.alpha_zscore != null && (
|
||||||
|
<MetricTooltip metricKey="alpha_zscore">
|
||||||
|
<HStack spacing={1} cursor="help">
|
||||||
|
<ZScoreBar
|
||||||
|
value={alert.alpha_zscore}
|
||||||
|
color={alert.alpha_zscore >= 0 ? '#ff4d4f' : '#52c41a'}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="medium"
|
||||||
|
color={alert.alpha_zscore >= 0 ? '#ff4d4f' : '#52c41a'}
|
||||||
|
>
|
||||||
|
{alert.alpha_zscore >= 0 ? '+' : ''}{alert.alpha_zscore.toFixed(1)}σ
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</MetricTooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 成交额强度 */}
|
||||||
|
{alert.is_v2 && alert.amt_zscore != null && alert.amt_zscore > 0.5 && (
|
||||||
|
<MetricTooltip metricKey="amt_zscore">
|
||||||
|
<HStack spacing={1} cursor="help">
|
||||||
|
<Icon as={BarChart3} boxSize={3} color="#eb2f96" />
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="medium"
|
||||||
|
color={alert.amt_zscore >= 2 ? '#eb2f96' : '#faad14'}
|
||||||
|
>
|
||||||
|
{alert.amt_zscore.toFixed(1)}σ
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</MetricTooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 涨停占比 */}
|
||||||
|
{(alert.limit_up_ratio || 0) > 0.03 && (
|
||||||
|
<MetricTooltip metricKey="limit_up_ratio">
|
||||||
|
<HStack
|
||||||
|
spacing={1}
|
||||||
|
color={alert.limit_up_ratio >= 0.1 ? '#ff4d4f' : '#fa8c16'}
|
||||||
|
cursor="help"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
as={Flame}
|
||||||
|
boxSize={3}
|
||||||
|
animation={alert.limit_up_ratio >= 0.15 ? `${pulseGlow} 2s infinite` : undefined}
|
||||||
|
/>
|
||||||
|
<Text fontSize="xs" fontWeight="bold">
|
||||||
|
{Math.round(alert.limit_up_ratio * 100)}%
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</MetricTooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 动量指标 */}
|
||||||
|
{alert.is_v2 && alert.momentum_3m != null && Math.abs(alert.momentum_3m) > 0.3 && (
|
||||||
|
<MetricTooltip metricKey="momentum_3m">
|
||||||
|
<HStack spacing={1} cursor="help">
|
||||||
|
<Icon as={Sparkles} boxSize={3} color="#1890ff" />
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="medium"
|
||||||
|
color={alert.momentum_3m >= 0 ? '#ff4d4f' : '#52c41a'}
|
||||||
|
>
|
||||||
|
{alert.momentum_3m >= 0 ? '+' : ''}{alert.momentum_3m.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</MetricTooltip>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 触发规则标签 */}
|
||||||
|
{alert.triggered_rules && alert.triggered_rules.length > 0 && (
|
||||||
|
<Flex mt={2} gap={1} flexWrap="wrap">
|
||||||
|
{alert.triggered_rules.slice(0, 4).map((rule, idx) => (
|
||||||
|
<TriggeredRuleBadge key={idx} rule={rule} />
|
||||||
|
))}
|
||||||
|
{alert.triggered_rules.length > 4 && (
|
||||||
|
<Text fontSize="10px" color={subTextColor}>
|
||||||
|
+{alert.triggered_rules.length - 4}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 展开的股票列表 */}
|
{/* 展开的股票列表 */}
|
||||||
@@ -183,12 +528,12 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
|||||||
borderTopWidth="1px"
|
borderTopWidth="1px"
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
p={3}
|
p={3}
|
||||||
bg={tableBg}
|
bg={expandedBg}
|
||||||
>
|
>
|
||||||
{loadingStocks ? (
|
{loadingStocks ? (
|
||||||
<HStack justify="center" py={4}>
|
<HStack justify="center" py={4}>
|
||||||
<Spinner size="sm" color="purple.500" />
|
<Spinner size="sm" color={alertConfig.color} />
|
||||||
<Text fontSize="sm" color="gray.500">加载相关股票...</Text>
|
<Text fontSize="sm" color={subTextColor}>加载相关股票...</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
) : stocks && stocks.length > 0 ? (
|
) : stocks && stocks.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
@@ -200,28 +545,43 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
|||||||
const upCount = validStocks.filter(s => s.change_pct > 0).length;
|
const upCount = validStocks.filter(s => s.change_pct > 0).length;
|
||||||
const downCount = validStocks.filter(s => s.change_pct < 0).length;
|
const downCount = validStocks.filter(s => s.change_pct < 0).length;
|
||||||
return (
|
return (
|
||||||
<HStack spacing={4} mb={2} fontSize="xs" color="gray.500">
|
<HStack
|
||||||
|
spacing={4}
|
||||||
|
mb={3}
|
||||||
|
p={2}
|
||||||
|
bg={tableBg}
|
||||||
|
borderRadius="lg"
|
||||||
|
fontSize="xs"
|
||||||
|
>
|
||||||
<HStack>
|
<HStack>
|
||||||
<Text>概念均涨:</Text>
|
<Text color={subTextColor}>板块均涨:</Text>
|
||||||
<Text fontWeight="bold" color={avgChange >= 0 ? 'red.400' : 'green.400'}>
|
<Text fontWeight="bold" color={avgChange >= 0 ? '#ff4d4f' : '#52c41a'}>
|
||||||
{avgChange >= 0 ? '+' : ''}{avgChange.toFixed(2)}%
|
{avgChange >= 0 ? '+' : ''}{avgChange.toFixed(2)}%
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
<Box w="1px" h="12px" bg={borderColor} />
|
||||||
<HStack spacing={1}>
|
<HStack spacing={1}>
|
||||||
<Text color="red.400">{upCount}涨</Text>
|
<Text color="#ff4d4f" fontWeight="medium">{upCount}涨</Text>
|
||||||
<Text>/</Text>
|
<Text color={subTextColor}>/</Text>
|
||||||
<Text color="green.400">{downCount}跌</Text>
|
<Text color="#52c41a" fontWeight="medium">{downCount}跌</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
<TableContainer maxH="200px" overflowY="auto">
|
<TableContainer maxH="200px" overflowY="auto">
|
||||||
<Table size="sm" variant="simple">
|
<Table size="sm" variant="simple">
|
||||||
<Thead position="sticky" top={0} bg={tableBg} zIndex={1}>
|
<Thead position="sticky" top={0} bg={expandedBg} zIndex={1}>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th px={2} py={1} fontSize="xs" color="gray.500">股票</Th>
|
<Th px={2} py={2} fontSize="xs" color={subTextColor} borderColor={borderColor}>
|
||||||
<Th px={2} py={1} fontSize="xs" color="gray.500" isNumeric>涨跌</Th>
|
股票
|
||||||
<Th px={2} py={1} fontSize="xs" color="gray.500" maxW="120px">原因</Th>
|
</Th>
|
||||||
|
<Th px={2} py={2} fontSize="xs" color={subTextColor} isNumeric borderColor={borderColor}>
|
||||||
|
涨跌幅
|
||||||
|
</Th>
|
||||||
|
<Th px={2} py={2} fontSize="xs" color={subTextColor} maxW="120px" borderColor={borderColor}>
|
||||||
|
原因
|
||||||
|
</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -236,15 +596,16 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
|||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
_hover={{ bg: hoverBg }}
|
_hover={{ bg: hoverBg }}
|
||||||
onClick={(e) => handleStockClick(e, stockCode)}
|
onClick={(e) => handleStockClick(e, stockCode)}
|
||||||
|
transition="background 0.15s"
|
||||||
>
|
>
|
||||||
<Td px={2} py={1.5}>
|
<Td px={2} py={2} borderColor={borderColor}>
|
||||||
<Popover trigger="hover" placement="right" isLazy>
|
<Popover trigger="hover" placement="right" isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Text
|
<Text
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
color="cyan.400"
|
color="#1890ff"
|
||||||
fontWeight="medium"
|
fontWeight="medium"
|
||||||
_hover={{ textDecoration: 'underline' }}
|
_hover={{ color: '#40a9ff', textDecoration: 'underline' }}
|
||||||
>
|
>
|
||||||
{stockName}
|
{stockName}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -255,11 +616,13 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
|||||||
h="80px"
|
h="80px"
|
||||||
bg={popoverBg}
|
bg={popoverBg}
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
boxShadow="lg"
|
boxShadow="xl"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<PopoverBody p={2}>
|
<PopoverBody p={2}>
|
||||||
<Text fontSize="xs" color="gray.500" mb={1}>{stockName} 分时</Text>
|
<Text fontSize="xs" color={subTextColor} mb={1}>
|
||||||
|
{stockName} 分时
|
||||||
|
</Text>
|
||||||
<Box h="50px">
|
<Box h="50px">
|
||||||
<MiniTimelineChart stockCode={stockCode} />
|
<MiniTimelineChart stockCode={stockCode} />
|
||||||
</Box>
|
</Box>
|
||||||
@@ -268,13 +631,13 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
|||||||
</Portal>
|
</Portal>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Td>
|
</Td>
|
||||||
<Td px={2} py={1.5} isNumeric>
|
<Td px={2} py={2} isNumeric borderColor={borderColor}>
|
||||||
<Text
|
<Text
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
color={
|
color={
|
||||||
hasChange && changePct > 0 ? 'red.400' :
|
hasChange && changePct > 0 ? '#ff4d4f' :
|
||||||
hasChange && changePct < 0 ? 'green.400' : 'gray.400'
|
hasChange && changePct < 0 ? '#52c41a' : subTextColor
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{hasChange
|
{hasChange
|
||||||
@@ -283,8 +646,8 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
|||||||
}
|
}
|
||||||
</Text>
|
</Text>
|
||||||
</Td>
|
</Td>
|
||||||
<Td px={2} py={1.5} maxW="120px">
|
<Td px={2} py={2} maxW="120px" borderColor={borderColor}>
|
||||||
<Text fontSize="xs" color="gray.500" noOfLines={1}>
|
<Text fontSize="xs" color={subTextColor} noOfLines={1}>
|
||||||
{stock.reason || '-'}
|
{stock.reason || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</Td>
|
</Td>
|
||||||
@@ -293,15 +656,16 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => {
|
|||||||
})}
|
})}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
{stocks.length > 10 && (
|
|
||||||
<Text fontSize="xs" color="gray.500" textAlign="center" mt={2}>
|
|
||||||
共 {stocks.length} 只相关股票,显示前 10 只
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|
||||||
|
{stocks.length > 10 && (
|
||||||
|
<Text fontSize="xs" color={subTextColor} textAlign="center" mt={2}>
|
||||||
|
共 {stocks.length} 只相关股票,显示前 10 只
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Text fontSize="sm" color="gray.500" textAlign="center" py={2}>
|
<Text fontSize="sm" color={subTextColor} textAlign="center" py={2}>
|
||||||
暂无相关股票数据
|
暂无相关股票数据
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -320,6 +684,7 @@ const ConceptAlertList = ({ alerts = [], onAlertClick, selectedAlert, maxHeight
|
|||||||
const [loadingConcepts, setLoadingConcepts] = useState({});
|
const [loadingConcepts, setLoadingConcepts] = useState({});
|
||||||
|
|
||||||
const subTextColor = useColorModeValue('gray.500', 'gray.400');
|
const subTextColor = useColorModeValue('gray.500', 'gray.400');
|
||||||
|
const emptyBg = useColorModeValue('gray.50', '#111111');
|
||||||
|
|
||||||
// 获取概念相关股票
|
// 获取概念相关股票
|
||||||
const fetchConceptStocks = useCallback(async (conceptId) => {
|
const fetchConceptStocks = useCallback(async (conceptId) => {
|
||||||
@@ -330,29 +695,18 @@ const ConceptAlertList = ({ alerts = [], onAlertClick, selectedAlert, maxHeight
|
|||||||
setLoadingConcepts(prev => ({ ...prev, [conceptId]: true }));
|
setLoadingConcepts(prev => ({ ...prev, [conceptId]: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用后端 API 获取概念股票
|
const response = await axios.get(`/api/concept/${encodeURIComponent(conceptId)}/stocks`);
|
||||||
const response = await axios.get(`/api/concept/${conceptId}/stocks`);
|
|
||||||
if (response.data?.success && response.data?.data?.stocks) {
|
if (response.data?.success && response.data?.data?.stocks) {
|
||||||
setConceptStocks(prev => ({
|
setConceptStocks(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[conceptId]: response.data.data.stocks
|
[conceptId]: response.data.data.stocks
|
||||||
}));
|
}));
|
||||||
|
} else {
|
||||||
|
setConceptStocks(prev => ({ ...prev, [conceptId]: [] }));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取概念股票失败:', error);
|
console.error('获取概念股票失败:', error);
|
||||||
// 如果 API 失败,尝试从 ES 直接获取
|
setConceptStocks(prev => ({ ...prev, [conceptId]: [] }));
|
||||||
try {
|
|
||||||
const esResponse = await axios.get(`/api/es/concept/${conceptId}`);
|
|
||||||
if (esResponse.data?.stocks) {
|
|
||||||
setConceptStocks(prev => ({
|
|
||||||
...prev,
|
|
||||||
[conceptId]: esResponse.data.stocks
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (esError) {
|
|
||||||
console.error('ES 获取也失败:', esError);
|
|
||||||
setConceptStocks(prev => ({ ...prev, [conceptId]: [] }));
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingConcepts(prev => ({ ...prev, [conceptId]: false }));
|
setLoadingConcepts(prev => ({ ...prev, [conceptId]: false }));
|
||||||
}
|
}
|
||||||
@@ -366,19 +720,23 @@ const ConceptAlertList = ({ alerts = [], onAlertClick, selectedAlert, maxHeight
|
|||||||
setExpandedId(null);
|
setExpandedId(null);
|
||||||
} else {
|
} else {
|
||||||
setExpandedId(alertKey);
|
setExpandedId(alertKey);
|
||||||
// 获取股票数据
|
|
||||||
if (alert.concept_id) {
|
if (alert.concept_id) {
|
||||||
fetchConceptStocks(alert.concept_id);
|
fetchConceptStocks(alert.concept_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通知父组件
|
|
||||||
onAlertClick?.(alert);
|
onAlertClick?.(alert);
|
||||||
}, [expandedId, fetchConceptStocks, onAlertClick]);
|
}, [expandedId, fetchConceptStocks, onAlertClick]);
|
||||||
|
|
||||||
if (!alerts || alerts.length === 0) {
|
if (!alerts || alerts.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Box p={4} textAlign="center">
|
<Box
|
||||||
|
p={6}
|
||||||
|
textAlign="center"
|
||||||
|
bg={emptyBg}
|
||||||
|
borderRadius="xl"
|
||||||
|
>
|
||||||
|
<Icon as={Zap} boxSize={8} color={subTextColor} mb={2} opacity={0.5} />
|
||||||
<Text color={subTextColor} fontSize="sm">
|
<Text color={subTextColor} fontSize="sm">
|
||||||
当日暂无概念异动
|
当日暂无概念异动
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* 热点概览组件
|
* 热点概览组件 - 科技感设计
|
||||||
* 展示大盘分时走势 + 概念异动标注
|
* 展示大盘分时走势 + 概念异动标注
|
||||||
*
|
*
|
||||||
* 模块化结构:
|
* 模块化结构:
|
||||||
@@ -12,8 +12,6 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Card,
|
|
||||||
CardBody,
|
|
||||||
Heading,
|
Heading,
|
||||||
Text,
|
Text,
|
||||||
HStack,
|
HStack,
|
||||||
@@ -27,16 +25,36 @@ import {
|
|||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
Grid,
|
Grid,
|
||||||
GridItem,
|
GridItem,
|
||||||
Divider,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Collapse,
|
Collapse,
|
||||||
|
keyframes,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { FaFire, FaList, FaChartArea, FaChevronDown, FaChevronUp } from 'react-icons/fa';
|
import {
|
||||||
import { InfoIcon } from '@chakra-ui/icons';
|
Flame,
|
||||||
|
List,
|
||||||
|
LineChart,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Info,
|
||||||
|
Zap,
|
||||||
|
AlertCircle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { useHotspotData } from './hooks';
|
import { useHotspotData } from './hooks';
|
||||||
import { IndexMinuteChart, ConceptAlertList, AlertSummary } from './components';
|
import { IndexMinuteChart, ConceptAlertList, AlertSummary } from './components';
|
||||||
|
|
||||||
|
// 动画效果
|
||||||
|
const gradientShift = keyframes`
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const pulseGlow = keyframes`
|
||||||
|
0%, 100% { opacity: 0.5; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 热点概览主组件
|
* 热点概览主组件
|
||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
@@ -50,46 +68,102 @@ const HotspotOverview = ({ selectedDate }) => {
|
|||||||
const { loading, error, data } = useHotspotData(selectedDate);
|
const { loading, error, data } = useHotspotData(selectedDate);
|
||||||
|
|
||||||
// 颜色主题
|
// 颜色主题
|
||||||
const cardBg = useColorModeValue('white', '#1a1a1a');
|
const cardBg = useColorModeValue('white', '#0a0a0a');
|
||||||
const borderColor = useColorModeValue('gray.200', '#333333');
|
const borderColor = useColorModeValue('gray.200', '#1f1f1f');
|
||||||
const textColor = useColorModeValue('gray.800', 'white');
|
const textColor = useColorModeValue('gray.800', 'white');
|
||||||
const subTextColor = useColorModeValue('gray.600', 'gray.400');
|
const subTextColor = useColorModeValue('gray.600', 'gray.400');
|
||||||
|
const headerGradient = useColorModeValue(
|
||||||
|
'linear(to-r, orange.500, red.500)',
|
||||||
|
'linear(to-r, orange.400, red.400)'
|
||||||
|
);
|
||||||
|
|
||||||
// 点击异动标注
|
// 点击异动标注
|
||||||
const handleAlertClick = useCallback((alert) => {
|
const handleAlertClick = useCallback((alert) => {
|
||||||
setSelectedAlert(alert);
|
setSelectedAlert(alert);
|
||||||
// 可以在这里添加滚动到对应位置的逻辑
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 渲染加载状态
|
// 渲染加载状态
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Card bg={cardBg} borderWidth="1px" borderColor={borderColor}>
|
<Box
|
||||||
<CardBody>
|
bg={cardBg}
|
||||||
<Center h="400px">
|
borderRadius="2xl"
|
||||||
<VStack spacing={4}>
|
borderWidth="1px"
|
||||||
<Spinner size="xl" color="purple.500" thickness="4px" />
|
borderColor={borderColor}
|
||||||
<Text color={subTextColor}>加载热点概览数据...</Text>
|
overflow="hidden"
|
||||||
|
>
|
||||||
|
{/* 顶部装饰条 */}
|
||||||
|
<Box
|
||||||
|
h="4px"
|
||||||
|
bgGradient="linear(to-r, orange.400, red.500, pink.500)"
|
||||||
|
backgroundSize="200% 200%"
|
||||||
|
animation={`${gradientShift} 3s ease infinite`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Center h="400px" p={6}>
|
||||||
|
<VStack spacing={4}>
|
||||||
|
<Box position="relative">
|
||||||
|
<Spinner
|
||||||
|
size="xl"
|
||||||
|
color="orange.400"
|
||||||
|
thickness="3px"
|
||||||
|
speed="0.8s"
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
inset={0}
|
||||||
|
borderRadius="full"
|
||||||
|
animation={`${pulseGlow} 2s ease-in-out infinite`}
|
||||||
|
boxShadow="0 0 30px rgba(251, 146, 60, 0.3)"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<VStack spacing={1}>
|
||||||
|
<Text color={textColor} fontWeight="medium">
|
||||||
|
加载热点概览数据
|
||||||
|
</Text>
|
||||||
|
<Text color={subTextColor} fontSize="sm">
|
||||||
|
正在获取市场异动信息...
|
||||||
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Center>
|
</VStack>
|
||||||
</CardBody>
|
</Center>
|
||||||
</Card>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染错误状态
|
// 渲染错误状态
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Card bg={cardBg} borderWidth="1px" borderColor={borderColor}>
|
<Box
|
||||||
<CardBody>
|
bg={cardBg}
|
||||||
<Center h="400px">
|
borderRadius="2xl"
|
||||||
<VStack spacing={4}>
|
borderWidth="1px"
|
||||||
<Icon as={InfoIcon} boxSize={10} color="red.400" />
|
borderColor={borderColor}
|
||||||
<Text color="red.500">{error}</Text>
|
overflow="hidden"
|
||||||
|
>
|
||||||
|
<Box h="4px" bg="red.500" />
|
||||||
|
|
||||||
|
<Center h="400px" p={6}>
|
||||||
|
<VStack spacing={4}>
|
||||||
|
<Box
|
||||||
|
p={4}
|
||||||
|
borderRadius="full"
|
||||||
|
bg="red.500"
|
||||||
|
bgOpacity={0.1}
|
||||||
|
>
|
||||||
|
<Icon as={AlertCircle} boxSize={10} color="red.400" />
|
||||||
|
</Box>
|
||||||
|
<VStack spacing={1}>
|
||||||
|
<Text color="red.400" fontWeight="medium">
|
||||||
|
数据加载失败
|
||||||
|
</Text>
|
||||||
|
<Text color={subTextColor} fontSize="sm" textAlign="center">
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Center>
|
</VStack>
|
||||||
</CardBody>
|
</Center>
|
||||||
</Card>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,53 +175,131 @@ const HotspotOverview = ({ selectedDate }) => {
|
|||||||
const { index, alerts, alert_summary } = data;
|
const { index, alerts, alert_summary } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card bg={cardBg} borderWidth="1px" borderColor={borderColor}>
|
<Box
|
||||||
<CardBody>
|
bg={cardBg}
|
||||||
|
borderRadius="2xl"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
overflow="hidden"
|
||||||
|
transition="all 0.3s"
|
||||||
|
_hover={{
|
||||||
|
boxShadow: useColorModeValue(
|
||||||
|
'0 4px 20px rgba(0,0,0,0.08)',
|
||||||
|
'0 4px 20px rgba(0,0,0,0.4)'
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 顶部装饰条 */}
|
||||||
|
<Box
|
||||||
|
h="4px"
|
||||||
|
bgGradient="linear(to-r, orange.400, red.500, pink.500)"
|
||||||
|
backgroundSize="200% 200%"
|
||||||
|
animation={`${gradientShift} 3s ease infinite`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box p={5}>
|
||||||
{/* 头部 */}
|
{/* 头部 */}
|
||||||
<Flex align="center" mb={4}>
|
<Flex align="center" mb={5}>
|
||||||
<HStack spacing={3}>
|
<HStack spacing={3}>
|
||||||
<Icon as={FaFire} boxSize={6} color="orange.500" />
|
<Box
|
||||||
<Heading size="md" color={textColor}>
|
p={2}
|
||||||
热点概览
|
borderRadius="xl"
|
||||||
</Heading>
|
bgGradient="linear(to-br, orange.400, red.500)"
|
||||||
|
boxShadow="0 4px 15px rgba(251, 146, 60, 0.4)"
|
||||||
|
>
|
||||||
|
<Icon as={Flame} boxSize={5} color="white" />
|
||||||
|
</Box>
|
||||||
|
<VStack align="flex-start" spacing={0}>
|
||||||
|
<Heading size="md" color={textColor} fontWeight="bold">
|
||||||
|
热点概览
|
||||||
|
</Heading>
|
||||||
|
<Text fontSize="xs" color={subTextColor}>
|
||||||
|
实时概念异动监控
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
<Tooltip label={showAlertList ? '收起异动列表' : '展开异动列表'}>
|
{/* 异动数量徽章 */}
|
||||||
|
{alerts.length > 0 && (
|
||||||
|
<HStack
|
||||||
|
spacing={1}
|
||||||
|
px={3}
|
||||||
|
py={1.5}
|
||||||
|
borderRadius="full"
|
||||||
|
bg="orange.500"
|
||||||
|
bgOpacity={0.15}
|
||||||
|
>
|
||||||
|
<Icon as={Zap} boxSize={3.5} color="orange.400" />
|
||||||
|
<Text fontSize="sm" fontWeight="bold" color="orange.400">
|
||||||
|
{alerts.length}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 切换按钮 */}
|
||||||
|
<Tooltip label={showAlertList ? '收起异动列表' : '展开异动列表'} hasArrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={showAlertList ? <FaChevronUp /> : <FaList />}
|
icon={<Icon as={showAlertList ? ChevronUp : List} boxSize={4} />}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
borderRadius="lg"
|
||||||
onClick={() => setShowAlertList(!showAlertList)}
|
onClick={() => setShowAlertList(!showAlertList)}
|
||||||
aria-label="切换异动列表"
|
aria-label="切换异动列表"
|
||||||
|
_hover={{
|
||||||
|
bg: useColorModeValue('gray.100', 'gray.800'),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label="展示大盘走势与概念异动的关联">
|
|
||||||
<Icon as={InfoIcon} color={subTextColor} />
|
{/* 信息提示 */}
|
||||||
|
<Tooltip
|
||||||
|
label="展示大盘走势与概念异动的关联,帮助发现市场热点"
|
||||||
|
hasArrow
|
||||||
|
maxW="200px"
|
||||||
|
>
|
||||||
|
<Box cursor="help">
|
||||||
|
<Icon as={Info} color={subTextColor} boxSize={4} />
|
||||||
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 统计摘要 */}
|
{/* 统计摘要 */}
|
||||||
<Box mb={4}>
|
<Box mb={5}>
|
||||||
<AlertSummary indexData={index} alerts={alerts} alertSummary={alert_summary} />
|
<AlertSummary indexData={index} alerts={alerts} alertSummary={alert_summary} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Divider mb={4} />
|
|
||||||
|
|
||||||
{/* 主体内容:图表 + 异动列表 */}
|
{/* 主体内容:图表 + 异动列表 */}
|
||||||
<Grid
|
<Grid
|
||||||
templateColumns={{ base: '1fr', lg: showAlertList ? '1fr 300px' : '1fr' }}
|
templateColumns={{ base: '1fr', lg: showAlertList ? '1fr 320px' : '1fr' }}
|
||||||
gap={4}
|
gap={5}
|
||||||
>
|
>
|
||||||
{/* 分时图 */}
|
{/* 分时图 */}
|
||||||
<GridItem>
|
<GridItem>
|
||||||
<Box>
|
<Box
|
||||||
<HStack spacing={2} mb={2}>
|
bg={useColorModeValue('gray.50', '#0d0d0d')}
|
||||||
<Icon as={FaChartArea} color="purple.500" boxSize={4} />
|
borderRadius="xl"
|
||||||
<Text fontSize="sm" fontWeight="medium" color={textColor}>
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
p={4}
|
||||||
|
transition="all 0.2s"
|
||||||
|
>
|
||||||
|
<HStack spacing={2} mb={3}>
|
||||||
|
<Box
|
||||||
|
p={1.5}
|
||||||
|
borderRadius="lg"
|
||||||
|
bg="purple.500"
|
||||||
|
bgOpacity={0.15}
|
||||||
|
>
|
||||||
|
<Icon as={LineChart} boxSize={4} color="purple.400" />
|
||||||
|
</Box>
|
||||||
|
<Text fontSize="sm" fontWeight="bold" color={textColor}>
|
||||||
大盘分时走势
|
大盘分时走势
|
||||||
</Text>
|
</Text>
|
||||||
|
<Tooltip label="图表上的标记点表示概念异动时刻" hasArrow>
|
||||||
|
<Icon as={Info} boxSize={3} color={subTextColor} cursor="help" />
|
||||||
|
</Tooltip>
|
||||||
</HStack>
|
</HStack>
|
||||||
<IndexMinuteChart
|
<IndexMinuteChart
|
||||||
indexData={index}
|
indexData={index}
|
||||||
@@ -159,12 +311,26 @@ const HotspotOverview = ({ selectedDate }) => {
|
|||||||
</GridItem>
|
</GridItem>
|
||||||
|
|
||||||
{/* 异动列表(可收起) */}
|
{/* 异动列表(可收起) */}
|
||||||
<Collapse in={showAlertList} animateOpacity>
|
<Collapse in={showAlertList} animateOpacity style={{ overflow: 'visible' }}>
|
||||||
<GridItem>
|
<GridItem>
|
||||||
<Box>
|
<Box
|
||||||
<HStack spacing={2} mb={2}>
|
bg={useColorModeValue('gray.50', '#0d0d0d')}
|
||||||
<Icon as={FaList} color="orange.500" boxSize={4} />
|
borderRadius="xl"
|
||||||
<Text fontSize="sm" fontWeight="medium" color={textColor}>
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
p={4}
|
||||||
|
h="100%"
|
||||||
|
>
|
||||||
|
<HStack spacing={2} mb={3}>
|
||||||
|
<Box
|
||||||
|
p={1.5}
|
||||||
|
borderRadius="lg"
|
||||||
|
bg="orange.500"
|
||||||
|
bgOpacity={0.15}
|
||||||
|
>
|
||||||
|
<Icon as={List} boxSize={4} color="orange.400" />
|
||||||
|
</Box>
|
||||||
|
<Text fontSize="sm" fontWeight="bold" color={textColor}>
|
||||||
异动记录
|
异动记录
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color={subTextColor}>
|
<Text fontSize="xs" color={subTextColor}>
|
||||||
@@ -175,7 +341,7 @@ const HotspotOverview = ({ selectedDate }) => {
|
|||||||
alerts={alerts}
|
alerts={alerts}
|
||||||
onAlertClick={handleAlertClick}
|
onAlertClick={handleAlertClick}
|
||||||
selectedAlert={selectedAlert}
|
selectedAlert={selectedAlert}
|
||||||
maxHeight="350px"
|
maxHeight="380px"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
@@ -184,14 +350,22 @@ const HotspotOverview = ({ selectedDate }) => {
|
|||||||
|
|
||||||
{/* 无异动提示 */}
|
{/* 无异动提示 */}
|
||||||
{alerts.length === 0 && (
|
{alerts.length === 0 && (
|
||||||
<Center py={4}>
|
<Center
|
||||||
<Text color={subTextColor} fontSize="sm">
|
py={8}
|
||||||
当日暂无概念异动数据
|
mt={4}
|
||||||
</Text>
|
bg={useColorModeValue('gray.50', '#0d0d0d')}
|
||||||
|
borderRadius="xl"
|
||||||
|
>
|
||||||
|
<VStack spacing={2}>
|
||||||
|
<Icon as={Zap} boxSize={8} color={subTextColor} opacity={0.5} />
|
||||||
|
<Text color={subTextColor} fontSize="sm">
|
||||||
|
当日暂无概念异动数据
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
</Center>
|
</Center>
|
||||||
)}
|
)}
|
||||||
</CardBody>
|
</Box>
|
||||||
</Card>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,207 @@
|
|||||||
* 用于处理异动标注等图表相关逻辑
|
* 用于处理异动标注等图表相关逻辑
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异动类型配置 - 科技感配色方案
|
||||||
|
*/
|
||||||
|
export const ALERT_TYPE_CONFIG = {
|
||||||
|
surge_up: {
|
||||||
|
label: '急涨',
|
||||||
|
color: '#ff4d4f',
|
||||||
|
gradient: ['#ff4d4f', '#ff7a45'],
|
||||||
|
icon: 'TrendingUp',
|
||||||
|
bgAlpha: 0.15,
|
||||||
|
description: '概念板块出现快速上涨异动',
|
||||||
|
},
|
||||||
|
surge: {
|
||||||
|
label: '异动',
|
||||||
|
color: '#ff7a45',
|
||||||
|
gradient: ['#ff7a45', '#ffa940'],
|
||||||
|
icon: 'Zap',
|
||||||
|
bgAlpha: 0.12,
|
||||||
|
description: '概念板块出现明显异动信号',
|
||||||
|
},
|
||||||
|
surge_down: {
|
||||||
|
label: '急跌',
|
||||||
|
color: '#52c41a',
|
||||||
|
gradient: ['#52c41a', '#73d13d'],
|
||||||
|
icon: 'TrendingDown',
|
||||||
|
bgAlpha: 0.15,
|
||||||
|
description: '概念板块出现快速下跌异动',
|
||||||
|
},
|
||||||
|
volume_surge_up: {
|
||||||
|
label: '放量急涨',
|
||||||
|
color: '#eb2f96',
|
||||||
|
gradient: ['#eb2f96', '#f759ab'],
|
||||||
|
icon: 'Activity',
|
||||||
|
bgAlpha: 0.15,
|
||||||
|
description: '成交量放大伴随价格急涨,资金关注度高',
|
||||||
|
},
|
||||||
|
shrink_surge_up: {
|
||||||
|
label: '缩量急涨',
|
||||||
|
color: '#722ed1',
|
||||||
|
gradient: ['#722ed1', '#9254de'],
|
||||||
|
icon: 'Rocket',
|
||||||
|
bgAlpha: 0.15,
|
||||||
|
description: '成交量萎缩但价格急涨,筹码锁定良好',
|
||||||
|
},
|
||||||
|
volume_oscillation: {
|
||||||
|
label: '放量震荡',
|
||||||
|
color: '#13c2c2',
|
||||||
|
gradient: ['#13c2c2', '#36cfc9'],
|
||||||
|
icon: 'Waves',
|
||||||
|
bgAlpha: 0.12,
|
||||||
|
description: '成交量放大但价格震荡,多空分歧加大',
|
||||||
|
},
|
||||||
|
limit_up: {
|
||||||
|
label: '涨停潮',
|
||||||
|
color: '#fa541c',
|
||||||
|
gradient: ['#fa541c', '#ff7a45'],
|
||||||
|
icon: 'Flame',
|
||||||
|
bgAlpha: 0.15,
|
||||||
|
description: '板块内多只股票涨停,热度极高',
|
||||||
|
},
|
||||||
|
rank_jump: {
|
||||||
|
label: '排名跃升',
|
||||||
|
color: '#1890ff',
|
||||||
|
gradient: ['#1890ff', '#40a9ff'],
|
||||||
|
icon: 'ArrowUpCircle',
|
||||||
|
bgAlpha: 0.12,
|
||||||
|
description: '概念板块排名快速上升,关注度提升',
|
||||||
|
},
|
||||||
|
volume_spike: {
|
||||||
|
label: '放量异动',
|
||||||
|
color: '#faad14',
|
||||||
|
gradient: ['#faad14', '#ffc53d'],
|
||||||
|
icon: 'BarChart3',
|
||||||
|
bgAlpha: 0.12,
|
||||||
|
description: '成交量出现突发性放大',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指标配置 - 包含详细提示说明
|
||||||
|
*/
|
||||||
|
export const METRIC_CONFIG = {
|
||||||
|
final_score: {
|
||||||
|
label: '综合评分',
|
||||||
|
unit: '分',
|
||||||
|
tooltip: '综合规则评分和机器学习评分的最终得分,分数越高表示异动信号越强。60分以上值得关注,80分以上为强信号。',
|
||||||
|
format: (v) => Math.round(v),
|
||||||
|
getColor: (v) => {
|
||||||
|
if (v >= 80) return '#ff4d4f';
|
||||||
|
if (v >= 60) return '#fa8c16';
|
||||||
|
if (v >= 40) return '#faad14';
|
||||||
|
return '#8c8c8c';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rule_score: {
|
||||||
|
label: '规则评分',
|
||||||
|
unit: '分',
|
||||||
|
tooltip: '基于预设规则计算的评分,包括涨跌幅、成交量、涨停数等多维度指标。',
|
||||||
|
format: (v) => Math.round(v),
|
||||||
|
},
|
||||||
|
ml_score: {
|
||||||
|
label: 'AI评分',
|
||||||
|
unit: '分',
|
||||||
|
tooltip: '机器学习模型预测的评分,基于历史数据训练,预测该异动后续表现概率。',
|
||||||
|
format: (v) => Math.round(v),
|
||||||
|
},
|
||||||
|
confirm_ratio: {
|
||||||
|
label: '确认率',
|
||||||
|
unit: '%',
|
||||||
|
tooltip: '异动信号的确认程度。100%表示信号完全确认,数值越高表示异动越稳定、越可靠。低于60%的信号可能是噪音。',
|
||||||
|
format: (v) => Math.round(v * 100),
|
||||||
|
getColor: (v) => {
|
||||||
|
if (v >= 0.8) return '#52c41a';
|
||||||
|
if (v >= 0.6) return '#faad14';
|
||||||
|
return '#ff4d4f';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
alpha: {
|
||||||
|
label: '超额收益',
|
||||||
|
unit: '%',
|
||||||
|
tooltip: '概念板块相对于大盘的超额涨跌幅(Alpha)。正值表示跑赢大盘,负值表示跑输大盘。该指标反映板块的相对强弱。',
|
||||||
|
format: (v) => v?.toFixed(2),
|
||||||
|
getColor: (v) => v >= 0 ? '#ff4d4f' : '#52c41a',
|
||||||
|
showSign: true,
|
||||||
|
},
|
||||||
|
alpha_zscore: {
|
||||||
|
label: 'Alpha强度',
|
||||||
|
unit: 'σ',
|
||||||
|
tooltip: 'Alpha的Z-Score标准化值,衡量超额收益的统计显著性。|值|>2表示异常强,|值|>1.5表示较强。正值表示异常上涨,负值表示异常下跌。',
|
||||||
|
format: (v) => v?.toFixed(2),
|
||||||
|
getColor: (v) => v >= 0 ? '#ff4d4f' : '#52c41a',
|
||||||
|
showSign: true,
|
||||||
|
},
|
||||||
|
amt_zscore: {
|
||||||
|
label: '成交额强度',
|
||||||
|
unit: 'σ',
|
||||||
|
tooltip: '成交额的Z-Score标准化值,衡量当前成交额相对于历史的异常程度。>2表示成交额异常放大,资金活跃度高。',
|
||||||
|
format: (v) => v?.toFixed(2),
|
||||||
|
getColor: (v) => {
|
||||||
|
if (v >= 2) return '#eb2f96';
|
||||||
|
if (v >= 1) return '#faad14';
|
||||||
|
return '#8c8c8c';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rank_zscore: {
|
||||||
|
label: '排名变化强度',
|
||||||
|
unit: 'σ',
|
||||||
|
tooltip: '板块排名变化的Z-Score值。正值表示排名上升速度异常,>2表示排名跃升显著。',
|
||||||
|
format: (v) => v?.toFixed(2),
|
||||||
|
showSign: true,
|
||||||
|
},
|
||||||
|
momentum_3m: {
|
||||||
|
label: '3分钟动量',
|
||||||
|
unit: '',
|
||||||
|
tooltip: '过去3分钟的价格动量指标,反映短期趋势强度。正值表示上涨动量,负值表示下跌动量。',
|
||||||
|
format: (v) => v?.toFixed(3),
|
||||||
|
getColor: (v) => v >= 0 ? '#ff4d4f' : '#52c41a',
|
||||||
|
showSign: true,
|
||||||
|
},
|
||||||
|
momentum_5m: {
|
||||||
|
label: '5分钟动量',
|
||||||
|
unit: '',
|
||||||
|
tooltip: '过去5分钟的价格动量指标,比3分钟动量更稳定,过滤掉更多噪音。',
|
||||||
|
format: (v) => v?.toFixed(3),
|
||||||
|
getColor: (v) => v >= 0 ? '#ff4d4f' : '#52c41a',
|
||||||
|
showSign: true,
|
||||||
|
},
|
||||||
|
limit_up_ratio: {
|
||||||
|
label: '涨停占比',
|
||||||
|
unit: '%',
|
||||||
|
tooltip: '板块内涨停股票数量占总股票数的比例。>10%表示板块热度高,>20%表示涨停潮。',
|
||||||
|
format: (v) => Math.round(v * 100),
|
||||||
|
getColor: (v) => {
|
||||||
|
if (v >= 0.2) return '#ff4d4f';
|
||||||
|
if (v >= 0.1) return '#fa8c16';
|
||||||
|
if (v >= 0.05) return '#faad14';
|
||||||
|
return '#8c8c8c';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发规则配置
|
||||||
|
*/
|
||||||
|
export const TRIGGERED_RULES_CONFIG = {
|
||||||
|
alpha_moderate: { label: 'Alpha中等', color: '#ff7a45', description: '超额收益达到中等水平' },
|
||||||
|
alpha_strong: { label: 'Alpha强', color: '#ff4d4f', description: '超额收益达到强势水平' },
|
||||||
|
amt_moderate: { label: '成交额中等', color: '#faad14', description: '成交额异常放大中等' },
|
||||||
|
amt_strong: { label: '成交额强', color: '#fa8c16', description: '成交额异常放大明显' },
|
||||||
|
limit_up_moderate: { label: '涨停中等', color: '#eb2f96', description: '涨停股票数量适中' },
|
||||||
|
limit_up_extreme: { label: '涨停极端', color: '#ff4d4f', description: '涨停股票数量很多' },
|
||||||
|
momentum_3m_moderate: { label: '3分钟动量', color: '#1890ff', description: '短期动量信号触发' },
|
||||||
|
momentum_3m_strong: { label: '3分钟强动量', color: '#096dd9', description: '短期强动量信号' },
|
||||||
|
combo_alpha_amt: { label: 'Alpha+成交额', color: '#722ed1', description: '超额收益和成交额双重确认' },
|
||||||
|
combo_alpha_limitup: { label: 'Alpha+涨停', color: '#eb2f96', description: '超额收益和涨停双重确认' },
|
||||||
|
early_session: { label: '早盘信号', color: '#13c2c2', description: '开盘30分钟内的异动' },
|
||||||
|
'decay:accelerating': { label: '加速中', color: '#52c41a', description: '异动正在加速' },
|
||||||
|
'decay:stable': { label: '稳定', color: '#1890ff', description: '异动保持稳定' },
|
||||||
|
'decay:fading': { label: '衰减中', color: '#8c8c8c', description: '异动正在衰减' },
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取异动标注的配色和符号
|
* 获取异动标注的配色和符号
|
||||||
* @param {string} alertType - 异动类型
|
* @param {string} alertType - 异动类型
|
||||||
@@ -10,42 +211,41 @@
|
|||||||
* @returns {Object} { color, symbol, symbolSize }
|
* @returns {Object} { color, symbol, symbolSize }
|
||||||
*/
|
*/
|
||||||
export const getAlertStyle = (alertType, importanceScore = 0.5) => {
|
export const getAlertStyle = (alertType, importanceScore = 0.5) => {
|
||||||
let color = '#ff6b6b';
|
const config = ALERT_TYPE_CONFIG[alertType] || ALERT_TYPE_CONFIG.surge;
|
||||||
let symbol = 'pin';
|
const baseSize = 30;
|
||||||
let symbolSize = 35;
|
const sizeBonus = Math.min(importanceScore * 20, 15);
|
||||||
|
|
||||||
|
let symbol = 'pin';
|
||||||
switch (alertType) {
|
switch (alertType) {
|
||||||
case 'surge_up':
|
case 'surge_up':
|
||||||
case 'surge':
|
case 'surge':
|
||||||
color = '#ff4757';
|
case 'volume_surge_up':
|
||||||
|
case 'shrink_surge_up':
|
||||||
symbol = 'triangle';
|
symbol = 'triangle';
|
||||||
symbolSize = 30 + Math.min(importanceScore * 20, 15);
|
|
||||||
break;
|
break;
|
||||||
case 'surge_down':
|
case 'surge_down':
|
||||||
color = '#2ed573';
|
symbol = 'path://M0,0 L10,0 L5,10 Z';
|
||||||
symbol = 'path://M0,0 L10,0 L5,10 Z'; // 向下三角形
|
|
||||||
symbolSize = 30 + Math.min(importanceScore * 20, 15);
|
|
||||||
break;
|
break;
|
||||||
case 'limit_up':
|
case 'limit_up':
|
||||||
color = '#ff6348';
|
|
||||||
symbol = 'diamond';
|
symbol = 'diamond';
|
||||||
symbolSize = 28;
|
|
||||||
break;
|
break;
|
||||||
case 'rank_jump':
|
case 'rank_jump':
|
||||||
color = '#3742fa';
|
|
||||||
symbol = 'circle';
|
symbol = 'circle';
|
||||||
symbolSize = 25;
|
|
||||||
break;
|
break;
|
||||||
case 'volume_spike':
|
case 'volume_spike':
|
||||||
color = '#ffa502';
|
case 'volume_oscillation':
|
||||||
symbol = 'rect';
|
symbol = 'rect';
|
||||||
symbolSize = 25;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
symbol = 'pin';
|
||||||
}
|
}
|
||||||
|
|
||||||
return { color, symbol, symbolSize };
|
return {
|
||||||
|
color: config.color,
|
||||||
|
gradient: config.gradient,
|
||||||
|
symbol,
|
||||||
|
symbolSize: baseSize + sizeBonus,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,16 +254,30 @@ export const getAlertStyle = (alertType, importanceScore = 0.5) => {
|
|||||||
* @returns {string} 显示标签
|
* @returns {string} 显示标签
|
||||||
*/
|
*/
|
||||||
export const getAlertTypeLabel = (alertType) => {
|
export const getAlertTypeLabel = (alertType) => {
|
||||||
const labels = {
|
return ALERT_TYPE_CONFIG[alertType]?.label || alertType || '异动';
|
||||||
surge: '急涨',
|
};
|
||||||
surge_up: '暴涨',
|
|
||||||
surge_down: '暴跌',
|
/**
|
||||||
limit_up: '涨停增加',
|
* 获取异动类型的详细描述
|
||||||
rank_jump: '排名跃升',
|
* @param {string} alertType - 异动类型
|
||||||
volume_spike: '放量',
|
* @returns {string} 描述
|
||||||
unknown: '异动',
|
*/
|
||||||
|
export const getAlertTypeDescription = (alertType) => {
|
||||||
|
return ALERT_TYPE_CONFIG[alertType]?.description || '概念板块出现异动信号';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取异动类型的配色
|
||||||
|
* @param {string} alertType - 异动类型
|
||||||
|
* @returns {Object} { color, gradient, bgAlpha }
|
||||||
|
*/
|
||||||
|
export const getAlertTypeColor = (alertType) => {
|
||||||
|
const config = ALERT_TYPE_CONFIG[alertType] || ALERT_TYPE_CONFIG.surge;
|
||||||
|
return {
|
||||||
|
color: config.color,
|
||||||
|
gradient: config.gradient,
|
||||||
|
bgAlpha: config.bgAlpha,
|
||||||
};
|
};
|
||||||
return labels[alertType] || alertType;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,7 +302,7 @@ export const getAlertMarkPoints = (alerts, times, prices, priceMax, maxCount = 1
|
|||||||
const timeIndex = times.indexOf(alert.time);
|
const timeIndex = times.indexOf(alert.time);
|
||||||
const price = timeIndex >= 0 ? prices[timeIndex] : (alert.index_price || priceMax);
|
const price = timeIndex >= 0 ? prices[timeIndex] : (alert.index_price || priceMax);
|
||||||
|
|
||||||
const { color, symbol, symbolSize } = getAlertStyle(
|
const { color, gradient, symbol, symbolSize } = getAlertStyle(
|
||||||
alert.alert_type,
|
alert.alert_type,
|
||||||
alert.final_score / 100 || alert.importance_score || 0.5
|
alert.final_score / 100 || alert.importance_score || 0.5
|
||||||
);
|
);
|
||||||
@@ -113,23 +327,33 @@ export const getAlertMarkPoints = (alerts, times, prices, priceMax, maxCount = 1
|
|||||||
symbol,
|
symbol,
|
||||||
symbolSize,
|
symbolSize,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color,
|
color: {
|
||||||
borderColor: '#fff',
|
type: 'linear',
|
||||||
borderWidth: 1,
|
x: 0, y: 0, x2: 0, y2: 1,
|
||||||
shadowBlur: 3,
|
colorStops: [
|
||||||
shadowColor: 'rgba(0,0,0,0.2)',
|
{ offset: 0, color: gradient[0] },
|
||||||
|
{ offset: 1, color: gradient[1] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
borderColor: 'rgba(255,255,255,0.8)',
|
||||||
|
borderWidth: 2,
|
||||||
|
shadowBlur: 8,
|
||||||
|
shadowColor: `${color}66`,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: isDown ? 'bottom' : 'top',
|
position: isDown ? 'bottom' : 'top',
|
||||||
formatter: '{b}',
|
formatter: '{b}',
|
||||||
fontSize: 9,
|
fontSize: 10,
|
||||||
color: '#333',
|
fontWeight: 500,
|
||||||
backgroundColor: isDown ? 'rgba(46, 213, 115, 0.9)' : 'rgba(255,255,255,0.9)',
|
color: isDown ? '#52c41a' : '#ff4d4f',
|
||||||
padding: [2, 4],
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
borderRadius: 2,
|
padding: [3, 6],
|
||||||
|
borderRadius: 4,
|
||||||
borderColor: color,
|
borderColor: color,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
|
shadowBlur: 4,
|
||||||
|
shadowColor: 'rgba(0,0,0,0.1)',
|
||||||
},
|
},
|
||||||
alertData: alert, // 存储原始数据
|
alertData: alert, // 存储原始数据
|
||||||
};
|
};
|
||||||
@@ -153,8 +377,45 @@ export const formatScore = (score) => {
|
|||||||
*/
|
*/
|
||||||
export const getScoreColor = (score) => {
|
export const getScoreColor = (score) => {
|
||||||
const s = score || 0;
|
const s = score || 0;
|
||||||
if (s >= 80) return '#ff4757';
|
if (s >= 80) return '#ff4d4f';
|
||||||
if (s >= 60) return '#ff6348';
|
if (s >= 60) return '#fa8c16';
|
||||||
if (s >= 40) return '#ffa502';
|
if (s >= 40) return '#faad14';
|
||||||
return '#747d8c';
|
return '#8c8c8c';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分数等级标签
|
||||||
|
* @param {number} score - 分数 (0-100)
|
||||||
|
* @returns {Object} { label, color }
|
||||||
|
*/
|
||||||
|
export const getScoreLevel = (score) => {
|
||||||
|
const s = score || 0;
|
||||||
|
if (s >= 80) return { label: '强信号', color: '#ff4d4f' };
|
||||||
|
if (s >= 60) return { label: '中等', color: '#fa8c16' };
|
||||||
|
if (s >= 40) return { label: '一般', color: '#faad14' };
|
||||||
|
return { label: '弱信号', color: '#8c8c8c' };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化指标值
|
||||||
|
* @param {string} metricKey - 指标键名
|
||||||
|
* @param {number} value - 值
|
||||||
|
* @returns {Object} { formatted, color, showSign }
|
||||||
|
*/
|
||||||
|
export const formatMetric = (metricKey, value) => {
|
||||||
|
const config = METRIC_CONFIG[metricKey];
|
||||||
|
if (!config || value === null || value === undefined) {
|
||||||
|
return { formatted: '-', color: '#8c8c8c', showSign: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = config.format(value);
|
||||||
|
const color = config.getColor ? config.getColor(value) : '#8c8c8c';
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatted: config.showSign && value > 0 ? `+${formatted}` : formatted,
|
||||||
|
color,
|
||||||
|
unit: config.unit,
|
||||||
|
label: config.label,
|
||||||
|
tooltip: config.tooltip,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user