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);
+};