| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- /**
- * DocumentOutline Component
- *
- * Displays a hierarchical document outline (table of contents) with:
- * - Nested heading structure
- * - Active item highlighting
- * - Click navigation to sections
- * - Section numbering
- *
- * @module components/DocumentOutline
- */
- import React from 'react';
- import { Tree } from 'antd';
- import type { DataNode } from 'antd/es/tree';
- import type { OutlineItem } from '../../services/documentContentService';
- import { FileTextOutlined } from '@ant-design/icons';
- // ── Styles ────────────────────────────────────────────────────────────────
- const containerStyle: React.CSSProperties = {
- height: '100%',
- display: 'flex',
- flexDirection: 'column',
- overflow: 'hidden',
- backgroundColor: '#fafafa',
- };
- const headerStyle: React.CSSProperties = {
- padding: '16px',
- borderBottom: '1px solid #e8e8e8',
- backgroundColor: '#ffffff',
- fontWeight: 600,
- fontSize: '14px',
- color: '#262626',
- display: 'flex',
- alignItems: 'center',
- gap: '8px',
- };
- const treeContainerStyle: React.CSSProperties = {
- flex: 1,
- overflow: 'auto',
- padding: '12px',
- };
- // ── Component Props ─────────────────────────────────────────────────────────
- export interface DocumentOutlineProps {
- /** Outline data (hierarchical structure) */
- outline: OutlineItem[];
- /** Currently active item ID */
- activeId?: string;
- /** Callback when outline item is clicked */
- onItemClick: (item: OutlineItem) => void;
- }
- // ── Helper Functions ────────────────────────────────────────────────────────
- /**
- * Convert OutlineItem to Ant Design Tree DataNode
- */
- const convertToTreeData = (items: OutlineItem[], onItemClick: (item: OutlineItem) => void): DataNode[] => {
- return items.map((item) => ({
- key: item.id,
- title: (
- <span
- style={{
- fontSize: '13px',
- color: '#595959',
- cursor: 'pointer',
- }}
- onClick={() => onItemClick(item)}
- >
- {item.sectionNumber && <span style={{ marginRight: '8px', color: '#8c8c8c' }}>{item.sectionNumber}</span>}
- {item.text}
- </span>
- ),
- children: item.children && item.children.length > 0 ? convertToTreeData(item.children, onItemClick) : undefined,
- }));
- };
- // ── Component ────────────────────────────────────────────────────────────────
- /**
- * DocumentOutline
- *
- * Renders a hierarchical outline of the document with navigation support.
- *
- * @example
- * ```tsx
- * <DocumentOutline
- * outline={documentOutline}
- * activeId="heading-5"
- * onItemClick={(item) => scrollToSection(item.id)}
- * />
- * ```
- */
- export const DocumentOutline: React.FC<DocumentOutlineProps> = ({
- outline,
- activeId,
- onItemClick,
- }) => {
- // Convert outline to tree data
- const treeData = convertToTreeData(outline, onItemClick);
- // Get all keys for default expansion
- const getAllKeys = (items: OutlineItem[]): string[] => {
- let keys: string[] = [];
- items.forEach((item) => {
- keys.push(item.id);
- if (item.children && item.children.length > 0) {
- keys = keys.concat(getAllKeys(item.children));
- }
- });
- return keys;
- };
- const expandedKeys = getAllKeys(outline);
- // Handle empty outline
- if (!outline || outline.length === 0) {
- return (
- <div style={containerStyle}>
- <div style={headerStyle}>
- <FileTextOutlined />
- <span>文档大纲</span>
- </div>
- <div style={{ ...treeContainerStyle, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
- <span style={{ color: '#8c8c8c', fontSize: '13px' }}>暂无大纲</span>
- </div>
- </div>
- );
- }
- return (
- <div style={containerStyle} data-testid="document-outline">
- {/* Header */}
- <div style={headerStyle}>
- <FileTextOutlined />
- <span>文档大纲</span>
- </div>
- {/* Tree */}
- <div style={treeContainerStyle}>
- <Tree
- treeData={treeData}
- defaultExpandAll
- expandedKeys={expandedKeys}
- selectedKeys={activeId ? [activeId] : []}
- showLine={{ showLeafIcon: false }}
- showIcon={false}
- blockNode
- style={{
- backgroundColor: 'transparent',
- fontSize: '13px',
- }}
- />
- </div>
- </div>
- );
- };
- export default DocumentOutline;
|