feat: 添加悬浮弹窗能力

This commit is contained in:
zdl
2025-10-15 18:22:02 +08:00
parent c88aafcc04
commit 0bc1892086
3 changed files with 375 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
// src/components/Citation/CitationMark.js
import React, { useState } from 'react';
import { Popover, Typography, Space, Divider } from 'antd';
import { FileTextOutlined, UserOutlined, CalendarOutlined } from '@ant-design/icons';
const { Text } = Typography;
/**
* 引用标记组件 - 显示上标引用【1】【2】【3】
* 支持悬浮(桌面)和点击(移动)两种交互方式
*
* @param {Object} props
* @param {number} props.citationId - 引用 ID1, 2, 3...
* @param {Object} props.citation - 引用数据对象
* @param {string} props.citation.author - 作者
* @param {string} props.citation.report_title - 报告标题
* @param {string} props.citation.declare_date - 发布日期
* @param {string} props.citation.sentences - 摘要片段
*/
const CitationMark = ({ citationId, citation }) => {
const [popoverVisible, setPopoverVisible] = useState(false);
// 如果没有引用数据,不渲染
if (!citation) {
return null;
}
// 引用卡片内容
const citationContent = (
<div style={{ maxWidth: 350, padding: '8px 4px' }}>
{/* 作者 */}
<Space align="start" style={{ marginBottom: 8 }}>
<UserOutlined style={{ color: '#1890ff', marginTop: 4 }} />
<div>
<Text type="secondary" style={{ fontSize: 12 }}>作者</Text>
<br />
<Text strong style={{ fontSize: 13 }}>{citation.author}</Text>
</div>
</Space>
<Divider style={{ margin: '8px 0' }} />
{/* 报告标题 */}
<Space align="start" style={{ marginBottom: 8 }}>
<FileTextOutlined style={{ color: '#52c41a', marginTop: 4 }} />
<div>
<Text type="secondary" style={{ fontSize: 12 }}>报告标题</Text>
<br />
<Text strong style={{ fontSize: 13 }}>{citation.report_title}</Text>
</div>
</Space>
<Divider style={{ margin: '8px 0' }} />
{/* 发布日期 */}
<Space align="start" style={{ marginBottom: 8 }}>
<CalendarOutlined style={{ color: '#faad14', marginTop: 4 }} />
<div>
<Text type="secondary" style={{ fontSize: 12 }}>发布日期</Text>
<br />
<Text style={{ fontSize: 13 }}>{citation.declare_date}</Text>
</div>
</Space>
<Divider style={{ margin: '8px 0' }} />
{/* 摘要片段 */}
<div>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>
摘要片段
</Text>
<Text
style={{
fontSize: 13,
lineHeight: 1.6,
display: 'block',
color: '#595959'
}}
>
{citation.sentences}
</Text>
</div>
</div>
);
// 检测是否为移动设备
const isMobile = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
};
// 移动端:仅点击触发
// 桌面端:悬浮 + 点击都触发
const triggerType = isMobile() ? 'click' : ['hover', 'click'];
return (
<Popover
content={citationContent}
title={`引用来源 [${citationId}]`}
trigger={triggerType}
placement="top"
overlayInnerStyle={{ maxWidth: 380 }}
open={popoverVisible}
onOpenChange={setPopoverVisible}
>
<sup
style={{
display: 'inline-block',
color: '#1890ff',
fontWeight: 'bold',
cursor: 'pointer',
padding: '0 2px',
fontSize: '0.85em',
userSelect: 'none',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
if (!isMobile()) {
e.target.style.color = '#40a9ff';
e.target.style.textDecoration = 'underline';
}
}}
onMouseLeave={(e) => {
if (!isMobile()) {
e.target.style.color = '#1890ff';
e.target.style.textDecoration = 'none';
}
}}
onClick={() => {
setPopoverVisible(!popoverVisible);
}}
>
{citationId}
</sup>
</Popover>
);
};
export default CitationMark;

View File

@@ -0,0 +1,104 @@
// src/components/Citation/CitedContent.js
import React from 'react';
import { Typography, Space, Tag } from 'antd';
import { RobotOutlined, FileSearchOutlined } from '@ant-design/icons';
import CitationMark from './CitationMark';
import { processCitationData } from '../../utils/citationUtils';
const { Text } = Typography;
/**
* 带引用标注的内容组件
* 展示拼接的文本每句话后显示上标引用【1】【2】【3】
* 支持鼠标悬浮和点击查看引用来源
*
* @param {Object} props
* @param {Object} props.data - API 返回的原始数据 { data: [...] }
* @param {string} props.title - 标题文本,默认 "AI 分析结果"
* @param {boolean} props.showAIBadge - 是否显示 AI 生成标识,默认 true
* @param {Object} props.containerStyle - 容器额外样式(可选)
*
* @example
* <CitedContent
* data={apiData}
* title="关联描述"
* showAIBadge={true}
* containerStyle={{ marginTop: 16 }}
* />
*/
const CitedContent = ({
data,
title = 'AI 分析结果',
showAIBadge = true,
containerStyle = {}
}) => {
// 处理数据
const processed = processCitationData(data);
// 如果数据无效,不渲染
if (!processed) {
console.warn('CitedContent: Invalid data, not rendering');
return null;
}
return (
<div
style={{
backgroundColor: '#f5f5f5',
borderRadius: 6,
padding: 16,
...containerStyle
}}
>
{/* 标题栏 */}
<Space
style={{
width: '100%',
justifyContent: 'space-between',
marginBottom: 12
}}
>
<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>
{/* 带引用的文本内容 */}
<div style={{ lineHeight: 1.8 }}>
{processed.segments.map((segment, index) => (
<React.Fragment key={`segment-${segment.citationId}`}>
{/* 文本片段 */}
<Text style={{ fontSize: 14 }}>
{segment.text}
</Text>
{/* 引用标记 */}
<CitationMark
citationId={segment.citationId}
citation={processed.citations[segment.citationId]}
/>
{/* 在片段之间添加逗号分隔符(最后一个不加) */}
{index < processed.segments.length - 1 && (
<Text style={{ fontSize: 14 }}></Text>
)}
</React.Fragment>
))}
</div>
</div>
);
};
export default CitedContent;