Skip to content

Commit 5240050

Browse files
committed
feat(documentation): 新增文档引用关系图谱并增强文档中心功能
- BacklinksPanel中每个链接项新增跳转按钮,提升用户交互体验 - DocumentCenter中支持所有文档索引缓存,实现跨集合内链查找和动态加载 - MarkdownRenderer新增内部链接点击事件处理,支持多种内部链接格式 - 新增DocumentGraph组件,基于canvas绘制交互式文档引用关系图谱 - DocumentGraph模块新增样式和响应式设计,支持图谱展开收起功能 - documentCenter工具库新增构建图谱及布局计算功能,支持最大跳数配置 - DocumentCenter集成DocumentGraph,实现边栏底部展示当前文档引用关系图谱 - 反向链接映射升级为全局所有文档层面,确保链接关系完整准确 - 优化样式,增强链接文字截断及按钮交互动画,提升整体界面美观和响应速度
1 parent ce317bb commit 5240050

7 files changed

Lines changed: 791 additions & 24 deletions

File tree

src/components/documentation/BacklinksPanel.jsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,21 @@ export function BacklinksPanel({ outgoing = [], incoming = [], onLinkClick }) {
5454
<li
5555
key={`${link.id}-${index}`}
5656
className={`${styles.linkItem} ${getLinkTypeClass(link.type)}`}
57-
onClick={() => onLinkClick && onLinkClick(link)}
5857
>
59-
<span className={styles.linkTitle}>{link.title}</span>
60-
<span className={styles.linkType}>{getLinkTypeLabel(link.type)}</span>
58+
<div className={styles.linkInfo} onClick={() => onLinkClick && onLinkClick(link)}>
59+
<span className={styles.linkTitle}>{link.title}</span>
60+
<span className={styles.linkType}>{getLinkTypeLabel(link.type)}</span>
61+
</div>
62+
<button
63+
className={styles.jumpButton}
64+
onClick={(e) => {
65+
e.stopPropagation();
66+
onLinkClick && onLinkClick(link);
67+
}}
68+
title="跳转到该文档"
69+
>
70+
71+
</button>
6172
</li>
6273
))}
6374
</ul>
@@ -76,10 +87,21 @@ export function BacklinksPanel({ outgoing = [], incoming = [], onLinkClick }) {
7687
<li
7788
key={`${link.id}-${index}`}
7889
className={`${styles.linkItem} ${getLinkTypeClass(link.type)}`}
79-
onClick={() => onLinkClick && onLinkClick(link)}
8090
>
81-
<span className={styles.linkTitle}>{link.title}</span>
82-
<span className={styles.linkType}>{getLinkTypeLabel(link.type)}</span>
91+
<div className={styles.linkInfo} onClick={() => onLinkClick && onLinkClick(link)}>
92+
<span className={styles.linkTitle}>{link.title}</span>
93+
<span className={styles.linkType}>{getLinkTypeLabel(link.type)}</span>
94+
</div>
95+
<button
96+
className={styles.jumpButton}
97+
onClick={(e) => {
98+
e.stopPropagation();
99+
onLinkClick && onLinkClick(link);
100+
}}
101+
title="跳转到该文档"
102+
>
103+
104+
</button>
83105
</li>
84106
))}
85107
</ul>

src/components/documentation/BacklinksPanel.module.css

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
border: 1px solid var(--border-color);
5656
cursor: pointer;
5757
transition: all var(--transition-base);
58+
gap: var(--spacing-sm);
5859
}
5960

6061
.linkItem:hover {
@@ -63,11 +64,23 @@
6364
transform: translateX(4px);
6465
}
6566

67+
.linkInfo {
68+
display: flex;
69+
justify-content: space-between;
70+
align-items: center;
71+
flex: 1;
72+
cursor: pointer;
73+
min-width: 0; /* 允许文本截断 */
74+
}
75+
6676
.linkTitle {
6777
font-size: 0.95rem;
6878
color: var(--text-primary);
6979
font-weight: 500;
7080
flex: 1;
81+
overflow: hidden;
82+
text-overflow: ellipsis;
83+
white-space: nowrap;
7184
}
7285

7386
.linkType {
@@ -77,6 +90,35 @@
7790
border-radius: var(--border-radius-sm);
7891
background-color: var(--bg-secondary);
7992
white-space: nowrap;
93+
margin-left: var(--spacing-sm);
94+
}
95+
96+
.jumpButton {
97+
display: flex;
98+
align-items: center;
99+
justify-content: center;
100+
width: 32px;
101+
height: 32px;
102+
border: 1px solid var(--border-color);
103+
background-color: var(--bg-secondary);
104+
color: var(--link-color);
105+
border-radius: var(--border-radius-sm);
106+
cursor: pointer;
107+
transition: all var(--transition-base);
108+
font-size: 1.2rem;
109+
font-weight: bold;
110+
flex-shrink: 0; /* 防止按钮被压缩 */
111+
}
112+
113+
.jumpButton:hover {
114+
background-color: var(--link-color);
115+
color: white;
116+
border-color: var(--link-color);
117+
transform: scale(1.1);
118+
}
119+
120+
.jumpButton:active {
121+
transform: scale(0.95);
80122
}
81123

82124
/* 不同类型的链接样式 */
@@ -127,13 +169,32 @@
127169
}
128170

129171
.linkItem {
172+
flex-wrap: wrap;
173+
gap: var(--spacing-xs);
174+
}
175+
176+
.linkInfo {
177+
flex: 1 1 100%;
130178
flex-direction: column;
131179
align-items: flex-start;
132180
gap: var(--spacing-xs);
133181
}
134182

135183
.linkType {
136-
align-self: flex-end;
184+
align-self: flex-start;
185+
margin-left: 0;
186+
}
187+
188+
.jumpButton {
189+
position: absolute;
190+
right: var(--spacing-md);
191+
top: 50%;
192+
transform: translateY(-50%);
193+
}
194+
195+
.linkItem {
196+
position: relative;
197+
padding-right: calc(var(--spacing-md) + 40px); /* 为按钮预留空间 */
137198
}
138199

139200
.stats {

src/components/documentation/DocumentCenter.jsx

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState, useEffect, useMemo } from 'react';
22
import { MarkdownRenderer } from '../markdown/MarkdownRenderer';
33
import { BacklinksPanel } from './BacklinksPanel';
4+
import { DocumentGraph } from './DocumentGraph';
45
import { loadAllMarkdownFiles, loadFolderMarkdownFiles } from '../../utils/markdownIndex';
56
import { filterMarkdownByLanguage } from '../../utils/i18nMarkdown';
67
import { useI18n } from '../../i18n/I18nContext';
@@ -11,7 +12,8 @@ import {
1112
findDocumentById,
1213
getDocumentPath as getDocPath,
1314
buildBacklinksMap,
14-
getDocumentLinks
15+
getDocumentLinks,
16+
enhanceDocument
1517
} from '../../utils/documentCenter';
1618
import styles from './DocumentCenter.module.css';
1719

@@ -46,6 +48,7 @@ export function DocumentCenter({
4648
className = ''
4749
}) {
4850
const [documents, setDocuments] = useState([]);
51+
const [allDocuments, setAllDocuments] = useState([]); // 所有文档索引,用于链接查找
4952
const [selectedDoc, setSelectedDoc] = useState(null);
5053
const [loading, setLoading] = useState(true);
5154
const [error, setError] = useState(null);
@@ -73,14 +76,19 @@ export function DocumentCenter({
7376
allFiles = await loadAllMarkdownFiles();
7477
}
7578

79+
// 增强所有文档(用于全局查找)
80+
const allEnhanced = allFiles.map(file => enhanceDocument(file));
81+
setAllDocuments(allEnhanced);
82+
7683
// 2. 根据类型过滤
84+
let filteredFiles = allFiles;
7785
if (type !== 'all') {
78-
allFiles = filterDocumentsByType(allFiles, type);
86+
filteredFiles = filterDocumentsByType(allFiles, type);
7987
}
8088

8189
// 3. 根据集合过滤
8290
if (collection) {
83-
allFiles = allFiles.filter(file => {
91+
filteredFiles = filteredFiles.filter(file => {
8492
// 检查 front matter 中的 collection 字段
8593
if (file.collection === collection) return true;
8694
// 检查文件夹名称
@@ -90,29 +98,34 @@ export function DocumentCenter({
9098
}
9199

92100
// 4. 根据语言过滤
93-
const filteredFiles = filterMarkdownByLanguage(allFiles, language, 'zh');
101+
const languageFiltered = filterMarkdownByLanguage(filteredFiles, language, 'zh');
94102

95103
// 5. 应用自定义过滤器
96104
let processedFiles = customFilter
97-
? filteredFiles.filter(customFilter)
98-
: filteredFiles;
105+
? languageFiltered.filter(customFilter)
106+
: languageFiltered;
99107

100108
// 6. 排序
101109
processedFiles = customSort
102110
? sortDocuments(processedFiles, customSort)
103111
: sortDocuments(processedFiles);
104112

105-
console.log(`文档中心加载: ${processedFiles.length} 个文档 (类型: ${type}, 集合: ${collection || '全部'})`);
113+
// 7. 增强文档(添加 ID 和 metadata)
114+
const enhancedFiles = processedFiles.map(file => enhanceDocument(file));
115+
116+
console.log(`文档中心加载: ${enhancedFiles.length} 个文档 (类型: ${type}, 集合: ${collection || '全部'})`);
117+
console.log('当前集合文档 ID:', enhancedFiles.map(d => d.id));
118+
console.log('所有文档 ID:', allEnhanced.map(d => d.id));
106119

107-
setDocuments(processedFiles);
120+
setDocuments(enhancedFiles);
108121

109-
// 构建反向链接映射
110-
const linksMap = buildBacklinksMap(processedFiles);
122+
// 构建反向链接映射(使用所有文档)
123+
const linksMap = buildBacklinksMap(allEnhanced);
111124
setBacklinksMap(linksMap);
112125

113126
// 默认选择第一个文档
114-
if (processedFiles.length > 0) {
115-
selectDocument(processedFiles[0]);
127+
if (enhancedFiles.length > 0) {
128+
selectDocument(enhancedFiles[0]);
116129
}
117130
} catch (err) {
118131
console.error('加载文档失败:', err);
@@ -146,11 +159,41 @@ export function DocumentCenter({
146159
}
147160
}
148161

162+
// 处理内部链接点击(从文档内容中)
163+
function handleInternalLinkClick(docId) {
164+
console.log(`点击内部链接, 目标 ID: ${docId}`);
165+
166+
// 首先在当前集合中查找
167+
let targetDoc = documents.find(d => d.id === docId);
168+
169+
// 如果当前集合中没有,在所有文档中查找
170+
if (!targetDoc && allDocuments.length > 0) {
171+
targetDoc = allDocuments.find(d => d.id === docId);
172+
if (targetDoc) {
173+
console.log(`在其他集合中找到目标文档: ${targetDoc.title} (collection: ${targetDoc.metadata?.collection || targetDoc.folder})`);
174+
// 将该文档添加到当前文档列表(临时)
175+
setDocuments(prev => [...prev, targetDoc]);
176+
}
177+
}
178+
179+
if (targetDoc) {
180+
console.log(`找到目标文档: ${targetDoc.title}`);
181+
selectDocument(targetDoc);
182+
// 滚动到顶部
183+
window.scrollTo({ top: 0, behavior: 'smooth' });
184+
} else {
185+
console.warn(`无法找到文档 ID: ${docId}`);
186+
console.log(`当前集合文档 ID:`, documents.map(d => `${d.id} (${d.title})`));
187+
console.log(`所有文档 ID:`, allDocuments.map(d => `${d.id} (${d.title})`));
188+
}
189+
}
190+
149191
// 获取当前文档的链接信息
150192
const currentDocLinks = useMemo(() => {
151193
if (!selectedDoc) return { outgoing: [], incoming: [] };
152-
return getDocumentLinks(selectedDoc, backlinksMap, documents);
153-
}, [selectedDoc, backlinksMap, documents]);
194+
// 使用所有文档来获取链接信息
195+
return getDocumentLinks(selectedDoc, backlinksMap, allDocuments);
196+
}, [selectedDoc, backlinksMap, allDocuments]);
154197

155198
// 切换树节点展开/折叠
156199
function toggleNode(nodeId) {
@@ -315,6 +358,16 @@ export function DocumentCenter({
315358
{t('documentCenter.navigation')}
316359
</h2>
317360
{renderDocumentList()}
361+
362+
{/* 引用关系图谱 - 放在侧边栏底部 */}
363+
{selectedDoc && (
364+
<DocumentGraph
365+
currentDoc={selectedDoc}
366+
backlinksMap={backlinksMap}
367+
allDocuments={allDocuments}
368+
onNodeClick={handleInternalLinkClick}
369+
/>
370+
)}
318371
</aside>
319372
)}
320373

@@ -339,7 +392,10 @@ export function DocumentCenter({
339392
)}
340393
</div>
341394
)}
342-
<MarkdownRenderer content={selectedDoc.content} />
395+
<MarkdownRenderer
396+
content={selectedDoc.content}
397+
onInternalLinkClick={handleInternalLinkClick}
398+
/>
343399

344400
{/* 双向链接面板 */}
345401
<BacklinksPanel

0 commit comments

Comments
 (0)