feat: 添加消息推送能力,添加新闻催化分析页的合规提示

This commit is contained in:
zdl
2025-10-21 10:59:52 +08:00
parent 300c0a18a6
commit ced6dbc559
15 changed files with 1800 additions and 125 deletions

View File

@@ -1,7 +1,7 @@
// src/components/Citation/CitedContent.js
import React from 'react';
import { Typography, Space, Tag } from 'antd';
import { RobotOutlined, FileSearchOutlined } from '@ant-design/icons';
import { Typography, Tag } from 'antd';
import { RobotOutlined } from '@ant-design/icons';
import CitationMark from './CitationMark';
import { processCitationData } from '../../utils/citationUtils';
import { logger } from '../../utils/logger';
@@ -9,20 +9,25 @@ import { logger } from '../../utils/logger';
const { Text } = Typography;
/**
* 带引用标注的内容组件
* 带引用标注的内容组件(块级模式)
* 展示拼接的文本每句话后显示上标引用【1】【2】【3】
* 支持鼠标悬浮和点击查看引用来源
* AI 标识统一显示在右上角,不占用布局高度
*
* @param {Object} props
* @param {Object} props.data - API 返回的原始数据 { data: [...] }
* @param {string} props.title - 标题文本,默认 "AI 分析结果"
* @param {boolean} props.showAIBadge - 是否显示 AI 生成标识,默认 true
* @param {string} props.prefix - 内容前的前缀标签,如 "机制:"(可选)
* @param {Object} props.prefixStyle - 前缀标签的自定义样式(可选)
* @param {boolean} props.showAIBadge - 是否显示右上角 AI 标识,默认 true可选
* @param {Object} props.containerStyle - 容器额外样式(可选)
*
* @example
* <CitedContent
* data={apiData}
* title="关联描述"
* prefix="机制:"
* prefixStyle={{ color: '#666' }}
* showAIBadge={true}
* containerStyle={{ marginTop: 16 }}
* />
@@ -30,6 +35,8 @@ const { Text } = Typography;
const CitedContent = ({
data,
title = 'AI 分析结果',
prefix = '',
prefixStyle = {},
showAIBadge = true,
containerStyle = {}
}) => {
@@ -45,59 +52,65 @@ const CitedContent = ({
return null;
}
// 判断是否显示标题栏内联模式title为空且不显示AI徽章
const showHeader = title || showAIBadge;
// 根据是否显示标题栏决定容器样式
const defaultContainerStyle = showHeader ? {
backgroundColor: '#f5f5f5',
borderRadius: 6,
padding: 16
} : {};
// 检查是否为内联模式
const isInlineMode = containerStyle?.display && containerStyle.display.includes('inline');
// 根据内联模式选择容器元素类型
const ContainerTag = isInlineMode ? 'span' : 'div';
const ContentTag = isInlineMode ? 'span' : 'div';
return (
<ContainerTag
<div
style={{
...defaultContainerStyle,
position: 'relative',
width: '100%',
backgroundColor: '#f5f5f5',
borderRadius: 6,
padding: 16,
paddingTop: title ? 16 : 20,
...containerStyle
}}
>
{/* 标题栏 - 仅在需要时显示 */}
{showHeader && (
<Space
{/* AI 标识 - 固定在右上角 */}
{showAIBadge && (
<Tag
icon={<RobotOutlined />}
color="purple"
style={{
width: '100%',
justifyContent: 'space-between',
marginBottom: 12
position: 'absolute',
top: 12,
right: 12,
margin: 0,
zIndex: 10,
fontSize: 12,
padding: '2px 8px'
}}
className="ai-badge-responsive"
>
<Space>
<FileSearchOutlined style={{ color: '#1890ff', fontSize: 16 }} />
<Text strong style={{ fontSize: 14 }}>
{title}
</Text>
</Space>
{showAIBadge && (
<Tag
icon={<RobotOutlined />}
color="purple"
style={{ margin: 0 }}
>
AI 生成
</Tag>
)}
</Space>
AI合成
</Tag>
)}
{/* 标题栏 */}
{title && (
<div style={{ marginBottom: 12, paddingRight: 80 }}>
<Text strong style={{ fontSize: 14 }}>
{title}
</Text>
</div>
)}
{/* 带引用的文本内容 */}
<ContentTag style={{ lineHeight: isInlineMode ? 'inherit' : 1.8 }}>
<div style={{
lineHeight: 1.8,
paddingRight: title ? 0 : (showAIBadge ? 80 : 0)
}}>
{/* 前缀标签(如果有) */}
{prefix && (
<Text style={{
fontSize: 14,
fontWeight: 'bold',
display: 'inline',
marginRight: 4,
...prefixStyle
}}>
{prefix}
</Text>
)}
{processed.segments.map((segment, index) => (
<React.Fragment key={`segment-${segment.citationId}`}>
{/* 文本片段 */}
@@ -117,8 +130,18 @@ const CitedContent = ({
)}
</React.Fragment>
))}
</ContentTag>
</ContainerTag>
</div>
{/* 响应式样式 */}
<style jsx>{`
@media (max-width: 768px) {
.ai-badge-responsive {
font-size: 10px !important;
padding: 1px 6px !important;
}
}
`}</style>
</div>
);
};

View File

@@ -0,0 +1,189 @@
// src/components/NotificationContainer/index.js
/**
* 通知容器组件 - 右下角层叠显示实时通知
*/
import React from 'react';
import {
Box,
VStack,
HStack,
Text,
IconButton,
Icon,
useColorModeValue,
Slide,
ScaleFade,
} from '@chakra-ui/react';
import { MdClose, MdCheckCircle, MdError, MdWarning, MdInfo } from 'react-icons/md';
import { useNotification } from '../../contexts/NotificationContext';
// 通知类型对应的图标和颜色
const NOTIFICATION_STYLES = {
success: {
icon: MdCheckCircle,
colorScheme: 'green',
bg: 'green.50',
borderColor: 'green.400',
iconColor: 'green.500',
},
error: {
icon: MdError,
colorScheme: 'red',
bg: 'red.50',
borderColor: 'red.400',
iconColor: 'red.500',
},
warning: {
icon: MdWarning,
colorScheme: 'orange',
bg: 'orange.50',
borderColor: 'orange.400',
iconColor: 'orange.500',
},
info: {
icon: MdInfo,
colorScheme: 'blue',
bg: 'blue.50',
borderColor: 'blue.400',
iconColor: 'blue.500',
},
};
/**
* 单个通知项组件
*/
const NotificationItem = ({ notification, onClose, isNewest = false }) => {
const { id, severity = 'info', title, message } = notification;
const style = NOTIFICATION_STYLES[severity] || NOTIFICATION_STYLES.info;
const bgColor = useColorModeValue(style.bg, `${style.colorScheme}.900`);
const borderColor = useColorModeValue(style.borderColor, `${style.colorScheme}.500`);
const textColor = useColorModeValue('gray.800', 'white');
const subTextColor = useColorModeValue('gray.600', 'gray.300');
return (
<ScaleFade initialScale={0.9} in={true}>
<Box
bg={bgColor}
borderLeft="4px solid"
borderColor={borderColor}
borderRadius="md"
boxShadow={isNewest ? '2xl' : 'lg'} // 最新消息更强的阴影
p={4}
minW="350px"
maxW="450px"
position="relative"
_hover={{
boxShadow: 'xl',
transform: 'translateX(-4px)',
}}
transition="all 0.2s"
// 最新消息添加微妙的高亮边框
{...(isNewest && {
borderRight: '1px solid',
borderRightColor: borderColor,
borderTop: '1px solid',
borderTopColor: useColorModeValue(`${style.colorScheme}.100`, `${style.colorScheme}.700`),
})}
>
<HStack spacing={3} align="start">
{/* 图标 */}
<Icon
as={style.icon}
w={6}
h={6}
color={style.iconColor}
mt={0.5}
flexShrink={0}
/>
{/* 内容 */}
<VStack align="start" spacing={1} flex={1} mr={6}>
<Text
fontSize="md"
fontWeight="bold"
color={textColor}
lineHeight="short"
>
{title}
</Text>
{message && (
<Text
fontSize="sm"
color={subTextColor}
lineHeight="short"
>
{message}
</Text>
)}
</VStack>
{/* 关闭按钮 */}
<IconButton
icon={<MdClose />}
size="sm"
variant="ghost"
colorScheme={style.colorScheme}
aria-label="关闭通知"
onClick={() => onClose(id)}
position="absolute"
top={2}
right={2}
_hover={{
bg: useColorModeValue(`${style.colorScheme}.100`, `${style.colorScheme}.800`),
}}
/>
</HStack>
</Box>
</ScaleFade>
);
};
/**
* 通知容器组件 - 主组件
*/
const NotificationContainer = () => {
const { notifications, removeNotification } = useNotification();
// 如果没有通知,不渲染
if (notifications.length === 0) {
return null;
}
return (
<Box
position="fixed"
bottom={6}
right={6}
zIndex={9999}
pointerEvents="none"
>
<VStack
spacing={3} // 消息之间间距 12px
align="flex-end"
pointerEvents="auto"
>
{notifications.map((notification, index) => (
<Slide
key={notification.id}
direction="right"
in={true}
style={{
position: 'relative',
zIndex: 9999 - index, // 最新消息index=0z-index最高
}}
>
<NotificationItem
notification={notification}
onClose={removeNotification}
isNewest={index === 0} // 第一条消息是最新的
/>
</Slide>
))}
</VStack>
</Box>
);
};
export default NotificationContainer;

View File

@@ -0,0 +1,231 @@
// src/components/NotificationTestTool/index.js
/**
* 通知测试工具 - 仅在开发环境显示
* 用于手动测试通知功能
*/
import React, { useState } from 'react';
import {
Box,
Button,
VStack,
HStack,
Text,
IconButton,
Collapse,
useDisclosure,
Badge,
} from '@chakra-ui/react';
import { MdNotifications, MdClose, MdVolumeOff, MdVolumeUp } from 'react-icons/md';
import { useNotification } from '../../contexts/NotificationContext';
import { SOCKET_TYPE } from '../../services/socket';
const NotificationTestTool = () => {
const { isOpen, onToggle } = useDisclosure();
const { addNotification, soundEnabled, toggleSound, isConnected, clearAllNotifications, notifications } = useNotification();
const [testCount, setTestCount] = useState(0);
// 只在开发环境显示
if (process.env.NODE_ENV !== 'development') {
return null;
}
const testNotifications = [
{
severity: 'success',
title: '买入成功',
message: '您的订单已成功执行:买入 贵州茅台(600519) 100股',
},
{
severity: 'error',
title: '委托失败',
message: '卖出订单失败:资金不足',
},
{
severity: 'warning',
title: '价格预警',
message: '您关注的股票已触达预设价格',
},
{
severity: 'info',
title: '持仓提醒',
message: '您持有的股票今日涨幅达 5.2%',
},
];
const handleTestNotification = (index) => {
const notif = testNotifications[index];
addNotification({
...notif,
type: 'trade_alert',
autoClose: 8000,
});
setTestCount(prev => prev + 1);
};
const handleMultipleNotifications = () => {
testNotifications.forEach((notif, index) => {
setTimeout(() => {
addNotification({
...notif,
type: 'trade_alert',
autoClose: 10000,
});
}, index * 600);
});
setTestCount(prev => prev + testNotifications.length);
};
const handleMaxLimitTest = () => {
// 测试最大限制快速发送6条验证只保留最新5条
for (let i = 1; i <= 6; i++) {
setTimeout(() => {
addNotification({
severity: i % 2 === 0 ? 'success' : 'info',
title: `测试消息 #${i}`,
message: `这是第 ${i} 条测试消息共6条应只保留最新5条`,
type: 'trade_alert',
autoClose: 12000,
});
}, i * 400);
}
setTestCount(prev => prev + 6);
};
return (
<Box
position="fixed"
top={4}
right={4}
zIndex={9998}
bg="white"
borderRadius="md"
boxShadow="lg"
overflow="hidden"
>
{/* 折叠按钮 */}
<HStack
p={2}
bg="blue.500"
color="white"
cursor="pointer"
onClick={onToggle}
spacing={2}
>
<MdNotifications size={20} />
<Text fontSize="sm" fontWeight="bold">
通知测试工具
</Text>
<Badge colorScheme={isConnected ? 'green' : 'red'} ml="auto">
{isConnected ? 'Connected' : 'Disconnected'}
</Badge>
<Badge colorScheme="purple">
{SOCKET_TYPE}
</Badge>
<IconButton
icon={isOpen ? <MdClose /> : <MdNotifications />}
size="xs"
variant="ghost"
colorScheme="whiteAlpha"
aria-label={isOpen ? '关闭' : '打开'}
/>
</HStack>
{/* 工具面板 */}
<Collapse in={isOpen} animateOpacity>
<VStack p={4} spacing={3} align="stretch" minW="250px">
<Text fontSize="xs" color="gray.600">
点击按钮测试不同类型的通知
</Text>
{/* 测试按钮 */}
<Button
size="sm"
colorScheme="green"
onClick={() => handleTestNotification(0)}
>
成功通知
</Button>
<Button
size="sm"
colorScheme="red"
onClick={() => handleTestNotification(1)}
>
错误通知
</Button>
<Button
size="sm"
colorScheme="orange"
onClick={() => handleTestNotification(2)}
>
警告通知
</Button>
<Button
size="sm"
colorScheme="blue"
onClick={() => handleTestNotification(3)}
>
信息通知
</Button>
<Button
size="sm"
colorScheme="purple"
onClick={handleMultipleNotifications}
>
层叠通知4
</Button>
<Button
size="sm"
colorScheme="pink"
onClick={handleMaxLimitTest}
>
测试最大限制65
</Button>
{/* 功能按钮 */}
<HStack spacing={2}>
<Button
size="sm"
variant="outline"
colorScheme="gray"
onClick={clearAllNotifications}
flex={1}
>
清空全部
</Button>
<IconButton
size="sm"
icon={soundEnabled ? <MdVolumeUp /> : <MdVolumeOff />}
colorScheme={soundEnabled ? 'blue' : 'gray'}
onClick={toggleSound}
aria-label="切换音效"
/>
</HStack>
{/* 统计信息 */}
<VStack spacing={1}>
<HStack justify="space-between" w="full">
<Text fontSize="xs" color="gray.500">
当前队列:
</Text>
<Badge colorScheme={notifications.length >= 5 ? 'red' : 'blue'}>
{notifications.length} / 5
</Badge>
</HStack>
<Text fontSize="xs" color="gray.400" textAlign="center">
已测试: {testCount} 条通知
</Text>
</VStack>
</VStack>
</Collapse>
</Box>
);
};
export default NotificationTestTool;

View File

@@ -553,7 +553,6 @@ const StockChartAntdModal = ({
<CitedContent
data={stock.relation_desc}
title="关联描述"
showAIBadge={true}
containerStyle={{ marginTop: 16 }}
/>
) : stock?.relation_desc ? (