From 0bc18920868e8fe77eaaab399cdc9f32a322d17a Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Wed, 15 Oct 2025 18:22:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=82=AC=E6=B5=AE?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Citation/CitationMark.js | 140 ++++++++++++++++++++++++ src/components/Citation/CitedContent.js | 104 ++++++++++++++++++ src/utils/citationUtils.js | 131 ++++++++++++++++++++++ 3 files changed, 375 insertions(+) create mode 100644 src/components/Citation/CitationMark.js create mode 100644 src/components/Citation/CitedContent.js create mode 100644 src/utils/citationUtils.js diff --git a/src/components/Citation/CitationMark.js b/src/components/Citation/CitationMark.js new file mode 100644 index 00000000..aa15e0a4 --- /dev/null +++ b/src/components/Citation/CitationMark.js @@ -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 - 引用 ID(1, 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 = ( +
+ {/* 作者 */} + + +
+ 作者 +
+ {citation.author} +
+
+ + + + {/* 报告标题 */} + + +
+ 报告标题 +
+ {citation.report_title} +
+
+ + + + {/* 发布日期 */} + + +
+ 发布日期 +
+ {citation.declare_date} +
+
+ + + + {/* 摘要片段 */} +
+ + 摘要片段 + + + {citation.sentences} + +
+
+ ); + + // 检测是否为移动设备 + const isMobile = () => { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent + ); + }; + + // 移动端:仅点击触发 + // 桌面端:悬浮 + 点击都触发 + const triggerType = isMobile() ? 'click' : ['hover', 'click']; + + return ( + + { + 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}】 + + + ); +}; + +export default CitationMark; diff --git a/src/components/Citation/CitedContent.js b/src/components/Citation/CitedContent.js new file mode 100644 index 00000000..1691e9f1 --- /dev/null +++ b/src/components/Citation/CitedContent.js @@ -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 + * + */ +const CitedContent = ({ + data, + title = 'AI 分析结果', + showAIBadge = true, + containerStyle = {} +}) => { + // 处理数据 + const processed = processCitationData(data); + + // 如果数据无效,不渲染 + if (!processed) { + console.warn('CitedContent: Invalid data, not rendering'); + return null; + } + + return ( +
+ {/* 标题栏 */} + + + + + {title} + + + {showAIBadge && ( + } + color="purple" + style={{ margin: 0 }} + > + AI 生成 + + )} + + + {/* 带引用的文本内容 */} +
+ {processed.segments.map((segment, index) => ( + + {/* 文本片段 */} + + {segment.text} + + + {/* 引用标记 */} + + + {/* 在片段之间添加逗号分隔符(最后一个不加) */} + {index < processed.segments.length - 1 && ( + + )} + + ))} +
+
+ ); +}; + +export default CitedContent; diff --git a/src/utils/citationUtils.js b/src/utils/citationUtils.js new file mode 100644 index 00000000..f966aa1d --- /dev/null +++ b/src/utils/citationUtils.js @@ -0,0 +1,131 @@ +// src/utils/citationUtils.js +// 引用数据处理工具 + +/** + * 处理后端返回的引用数据 + * + * @param {Object} rawData - 后端返回的原始数据 + * @param {Array} rawData.data - 引用数据数组 + * @returns {Object|null} 处理后的数据结构,包含 segments 和 citations + * + * @example + * 输入格式: + * { + * data: [ + * { + * author: "陈彤", + * sentences: "核心结论:...", + * query_part: "国内领先的IT解决方案提供商", + * match_score: "好", + * declare_date: "2025-04-17T00:00:00", + * report_title: "深度布局..." + * } + * ] + * } + * + * 输出格式: + * { + * segments: [ + * { text: "核心结论:...", citationId: 1 } + * ], + * citations: { + * 1: { + * author: "陈彤", + * report_title: "深度布局...", + * declare_date: "2025-04-17", + * sentences: "核心结论:..." + * } + * } + * } + */ +export const processCitationData = (rawData) => { + // 验证输入数据 + if (!rawData || !rawData.data || !Array.isArray(rawData.data)) { + console.warn('citationUtils: Invalid data format, expected { data: [...] }'); + return null; + } + + if (rawData.data.length === 0) { + console.warn('citationUtils: Empty data array'); + return null; + } + + const segments = []; + const citations = {}; + + // 处理每个引用数据项 + rawData.data.forEach((item, index) => { + // 验证必需字段 + if (!item.sentences) { + console.warn(`citationUtils: Missing 'sentences' field in item ${index}`); + return; + } + + const citationId = index + 1; // 引用 ID 从 1 开始 + + // 构建文本片段 + segments.push({ + text: item.sentences, + citationId: citationId + }); + + // 构建引用信息映射 + citations[citationId] = { + author: item.author || '未知作者', + report_title: item.report_title || '未知报告', + declare_date: formatDate(item.declare_date), + sentences: item.sentences, + // 保留原始数据以备扩展 + query_part: item.query_part, + match_score: item.match_score + }; + }); + + // 如果没有有效的片段,返回 null + if (segments.length === 0) { + console.warn('citationUtils: No valid segments found'); + return null; + } + + return { + segments, + citations + }; +}; + +/** + * 格式化日期 + * @param {string} dateStr - ISO 格式日期字符串 + * @returns {string} 格式化后的日期 YYYY-MM-DD + */ +const formatDate = (dateStr) => { + if (!dateStr) return '--'; + + try { + const date = new Date(dateStr); + if (isNaN(date.getTime())) return '--'; + + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; + } catch (e) { + console.warn('citationUtils: Date formatting error:', e); + return '--'; + } +}; + +/** + * 验证引用数据格式是否有效 + * @param {Object} data - 待验证的数据 + * @returns {boolean} 是否有效 + */ +export const isValidCitationData = (data) => { + if (!data || typeof data !== 'object') return false; + if (!data.data || !Array.isArray(data.data)) return false; + if (data.data.length === 0) return false; + + // 检查至少有一个有效的 sentences 字段 + return data.data.some(item => item && item.sentences); +};