DocumentOutline.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. /**
  2. * DocumentOutline Component
  3. *
  4. * Displays a hierarchical document outline (table of contents) with:
  5. * - Nested heading structure
  6. * - Active item highlighting
  7. * - Click navigation to sections
  8. * - Section numbering
  9. *
  10. * @module components/DocumentOutline
  11. */
  12. import React from 'react';
  13. import { Tree } from 'antd';
  14. import type { DataNode } from 'antd/es/tree';
  15. import type { OutlineItem } from '../../services/documentContentService';
  16. import { FileTextOutlined } from '@ant-design/icons';
  17. // ── Styles ────────────────────────────────────────────────────────────────
  18. const containerStyle: React.CSSProperties = {
  19. height: '100%',
  20. display: 'flex',
  21. flexDirection: 'column',
  22. overflow: 'hidden',
  23. backgroundColor: '#fafafa',
  24. };
  25. const headerStyle: React.CSSProperties = {
  26. padding: '16px',
  27. borderBottom: '1px solid #e8e8e8',
  28. backgroundColor: '#ffffff',
  29. fontWeight: 600,
  30. fontSize: '14px',
  31. color: '#262626',
  32. display: 'flex',
  33. alignItems: 'center',
  34. gap: '8px',
  35. };
  36. const treeContainerStyle: React.CSSProperties = {
  37. flex: 1,
  38. overflow: 'auto',
  39. padding: '12px',
  40. };
  41. // ── Component Props ─────────────────────────────────────────────────────────
  42. export interface DocumentOutlineProps {
  43. /** Outline data (hierarchical structure) */
  44. outline: OutlineItem[];
  45. /** Currently active item ID */
  46. activeId?: string;
  47. /** Callback when outline item is clicked */
  48. onItemClick: (item: OutlineItem) => void;
  49. }
  50. // ── Helper Functions ────────────────────────────────────────────────────────
  51. /**
  52. * Convert OutlineItem to Ant Design Tree DataNode
  53. */
  54. const convertToTreeData = (items: OutlineItem[], onItemClick: (item: OutlineItem) => void): DataNode[] => {
  55. return items.map((item) => ({
  56. key: item.id,
  57. title: (
  58. <span
  59. style={{
  60. fontSize: '13px',
  61. color: '#595959',
  62. cursor: 'pointer',
  63. }}
  64. onClick={() => onItemClick(item)}
  65. >
  66. {item.sectionNumber && <span style={{ marginRight: '8px', color: '#8c8c8c' }}>{item.sectionNumber}</span>}
  67. {item.text}
  68. </span>
  69. ),
  70. children: item.children && item.children.length > 0 ? convertToTreeData(item.children, onItemClick) : undefined,
  71. }));
  72. };
  73. // ── Component ────────────────────────────────────────────────────────────────
  74. /**
  75. * DocumentOutline
  76. *
  77. * Renders a hierarchical outline of the document with navigation support.
  78. *
  79. * @example
  80. * ```tsx
  81. * <DocumentOutline
  82. * outline={documentOutline}
  83. * activeId="heading-5"
  84. * onItemClick={(item) => scrollToSection(item.id)}
  85. * />
  86. * ```
  87. */
  88. export const DocumentOutline: React.FC<DocumentOutlineProps> = ({
  89. outline,
  90. activeId,
  91. onItemClick,
  92. }) => {
  93. // Convert outline to tree data
  94. const treeData = convertToTreeData(outline, onItemClick);
  95. // Get all keys for default expansion
  96. const getAllKeys = (items: OutlineItem[]): string[] => {
  97. let keys: string[] = [];
  98. items.forEach((item) => {
  99. keys.push(item.id);
  100. if (item.children && item.children.length > 0) {
  101. keys = keys.concat(getAllKeys(item.children));
  102. }
  103. });
  104. return keys;
  105. };
  106. const expandedKeys = getAllKeys(outline);
  107. // Handle empty outline
  108. if (!outline || outline.length === 0) {
  109. return (
  110. <div style={containerStyle}>
  111. <div style={headerStyle}>
  112. <FileTextOutlined />
  113. <span>文档大纲</span>
  114. </div>
  115. <div style={{ ...treeContainerStyle, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
  116. <span style={{ color: '#8c8c8c', fontSize: '13px' }}>暂无大纲</span>
  117. </div>
  118. </div>
  119. );
  120. }
  121. return (
  122. <div style={containerStyle} data-testid="document-outline">
  123. {/* Header */}
  124. <div style={headerStyle}>
  125. <FileTextOutlined />
  126. <span>文档大纲</span>
  127. </div>
  128. {/* Tree */}
  129. <div style={treeContainerStyle}>
  130. <Tree
  131. treeData={treeData}
  132. defaultExpandAll
  133. expandedKeys={expandedKeys}
  134. selectedKeys={activeId ? [activeId] : []}
  135. showLine={{ showLeafIcon: false }}
  136. showIcon={false}
  137. blockNode
  138. style={{
  139. backgroundColor: 'transparent',
  140. fontSize: '13px',
  141. }}
  142. />
  143. </div>
  144. </div>
  145. );
  146. };
  147. export default DocumentOutline;