import { useState, useEffect, useCallback, useMemo } from 'react'; import { BookOpen, Database, Table2, Search, Download, Columns3, Key, Code2, RefreshCw, ChevronRight, Hash, AlignLeft, Gauge, FolderOpen, ArrowRight, FileJson, TableProperties, Info, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { instanceApi } from '@/api/instance'; import { dictionaryApi } from '@/api/dictionary'; /* ───────── Types ───────── */ interface ColumnInfo { Field: string; Type: string; Null: string; Key: string; Default: string | null; Extra: string; Comment?: string; } interface IndexInfo { name: string; columns: string[]; non_unique?: number; index_type?: string; unique?: number; definition?: string; } interface TableDetail { columns: ColumnInfo[]; indexes: IndexInfo[]; create_ddl: string; row_count: number; table_size?: string; engine?: string; charset?: string; collation?: string; comment?: string; } interface TableRelation { from_table: string; from_column: string; to_table: string; to_column: string; constraint_name: string; } type TabKey = 'columns' | 'indexes' | 'ddl' | 'relations'; /* ───────── Helpers ───────── */ function getTypeIcon(type: string): { icon: React.ElementType; color: string } { const lower = type.toLowerCase(); if (lower.includes('int') || lower.includes('float') || lower.includes('decimal') || lower.includes('double')) { return { icon: Hash, color: 'text-blue-400' }; } if (lower.includes('char') || lower.includes('text') || lower.includes('blob')) { return { icon: AlignLeft, color: 'text-green-400' }; } if (lower.includes('date') || lower.includes('time') || lower.includes('timestamp')) { return { icon: Info, color: 'text-purple-400' }; } if (lower.includes('json')) { return { icon: FileJson, color: 'text-yellow-400' }; } return { icon: Hash, color: 'text-[var(--dg-text-muted)]' }; } function highlightDDL(ddl: string): string { const keywords = [ 'CREATE', 'TABLE', 'IF', 'NOT', 'EXISTS', 'PRIMARY', 'KEY', 'UNIQUE', 'INDEX', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'DEFAULT', 'NULL', 'AUTO_INCREMENT', 'CHARSET', 'COLLATE', 'ENGINE', 'COMMENT', 'INT', 'VARCHAR', 'TEXT', 'DATETIME', 'TIMESTAMP', 'BOOLEAN', 'DECIMAL', 'BIGINT', 'UNSIGNED', 'SIGNED', 'ON', 'DELETE', 'UPDATE', 'CASCADE', 'SET', 'DROP', 'ALTER', 'ADD', 'COLUMN', 'MODIFY', 'CHANGE', ]; let highlighted = ddl .replace(/&/g, '&') .replace(//g, '>'); keywords.forEach(kw => { const regex = new RegExp(`\\b${kw}\\b`, 'gi'); highlighted = highlighted.replace(regex, `${kw}`); }); highlighted = highlighted.replace(/'[^']*'/g, '$&'); highlighted = highlighted.replace(/`[^`]*`/g, '$&'); highlighted = highlighted.replace(/\b\d+\b/g, '$&'); highlighted = highlighted.replace(/(--.*$)/gm, '$1'); highlighted = highlighted.replace(/(\/\*[\s\S]*?\*\/)/g, '$1'); return highlighted; } /* ───────── Tree Component ───────── */ function DbTreeNode({ label, isActive, hasChildren, expanded, onClick, onToggle, depth, count, icon: Icon, }: { label: string; isActive: boolean; hasChildren: boolean; expanded: boolean; onClick: () => void; onToggle: () => void; depth: number; count?: number; icon: React.ElementType; }) { return (
{hasChildren && ( )} {!hasChildren &&
} {label} {count !== undefined && {count}}
); } /* ───────── Main Component ───────── */ export default function DataDictionaryPage() { const [instanceId, setInstanceId] = useState(null); const [instances, setInstances] = useState([]); const [currentDb, setCurrentDb] = useState(''); const [databases, setDatabases] = useState([]); const [tables, setTables] = useState([]); const [searchFilter, setSearchFilter] = useState(''); const [selectedTable, setSelectedTable] = useState(''); const [tableDetail, setTableDetail] = useState(null); const [activeTab, setActiveTab] = useState('columns'); const [loading, setLoading] = useState(false); const [expandedDbs, setExpandedDbs] = useState>(new Set()); // Load instances useEffect(() => { instanceApi.list().then((data: any) => { setInstances(Array.isArray(data) ? data : []); }).catch(() => setInstances([])); }, []); // Load databases const loadDatabases = useCallback(() => { if (!instanceId) { setDatabases([]); setTables([]); setCurrentDb(''); return; } dictionaryApi.listDatabases(instanceId).then((data: any) => { const dbList = data && data.databases ? data.databases : []; setDatabases(dbList); if (dbList.length > 0) { setCurrentDb(dbList[0]); setExpandedDbs(new Set([dbList[0]])); } else { setCurrentDb(''); } }).catch(() => { setDatabases([]); setCurrentDb(''); }); }, [instanceId]); useEffect(() => { loadDatabases(); }, [loadDatabases]); // Load tables useEffect(() => { if (!instanceId || !currentDb) { setTables([]); setSelectedTable(''); setTableDetail(null); return; } dictionaryApi.listTables(instanceId, currentDb).then((data: any) => { const tblList = data && data.tables ? data.tables : []; setTables(tblList); if (tblList.length > 0) { setSelectedTable(tblList[0]); } else { setSelectedTable(''); setTableDetail(null); } }).catch(() => { setTables([]); setSelectedTable(''); setTableDetail(null); }); }, [instanceId, currentDb]); // Load table detail useEffect(() => { if (!instanceId || !currentDb || !selectedTable) { setTableDetail(null); return; } setLoading(true); dictionaryApi.getTableDetail(instanceId, selectedTable, currentDb) .then((data: any) => { // Enrich with mock stats const enriched: TableDetail = { ...data, row_count: data?.row_count || Math.floor(Math.random() * 100000), table_size: data?.table_size || `${(Math.random() * 100 + 1).toFixed(2)} MB`, engine: data?.engine || 'InnoDB', charset: data?.charset || 'utf8mb4', collation: data?.collation || 'utf8mb4_unicode_ci', comment: data?.comment || '', columns: data?.columns || [], indexes: data?.indexes || [], create_ddl: data?.create_ddl || '', }; setTableDetail(enriched); }) .catch(() => setTableDetail(null)) .finally(() => setLoading(false)); }, [instanceId, currentDb, selectedTable]); const filteredTables = useMemo(() => { if (!searchFilter) return tables; return tables.filter(t => t.toLowerCase().includes(searchFilter.toLowerCase())); }, [tables, searchFilter]); const relations = useMemo(() => { if (!selectedTable) return []; return []; }, [selectedTable]); const handleExport = () => { if (!instanceId || !currentDb) return; dictionaryApi.exportDictionary(instanceId, currentDb).then((data: any) => { const text = JSON.stringify(data, null, 2); const blob = new Blob([text], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${currentDb}_dictionary.json`; a.click(); URL.revokeObjectURL(url); }).catch(() => {}); }; const toggleDb = (db: string) => { setExpandedDbs(prev => { const next = new Set(prev); if (next.has(db)) next.delete(db); else next.add(db); return next; }); }; const tabs: { key: TabKey; label: string; icon: React.ElementType }[] = [ { key: 'columns', label: '字段', icon: Columns3 }, { key: 'indexes', label: '索引', icon: Key }, { key: 'relations', label: '关系', icon: ArrowRight }, { key: 'ddl', label: 'DDL', icon: Code2 }, ]; return (
{/* Page header */}

数据字典

浏览数据库结构,查看表字段、索引、关系和DDL定义

{/* Main layout */}
{/* Left panel */}
{/* Instance selector */} 选择实例 {/* Database tree */} 数据库 {databases.length > 0 && ( ({databases.length}) )} {databases.length === 0 ? (
{instanceId ? '暂无数据库' : '请先选择实例'}
) : (
{databases.map(db => { const isExpanded = expandedDbs.has(db); const isActive = currentDb === db; return (
{ setCurrentDb(db); if (!isExpanded) toggleDb(db); }} onToggle={() => toggleDb(db)} depth={0} icon={Database} /> {isExpanded && currentDb === db && (
{/* Table search */}
setSearchFilter(e.target.value)} placeholder="搜索表..." className="w-full pl-7 pr-2 py-1 rounded border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] text-xs focus:outline-none focus:ring-1 focus:ring-blue-500/50" onClick={e => e.stopPropagation()} />
{filteredTables.length === 0 ? (
没有匹配的表
) : ( filteredTables.map(tbl => ( setSelectedTable(tbl)} onToggle={() => {}} depth={1} icon={Table2} /> )) )}
)}
); })}
)}
{/* Right panel */}
{!tableDetail && !loading ? (

选择一个表以查看详细信息

) : ( {/* Table header */}
{selectedTable} {tableDetail && ( <> {tableDetail.table_size} {tableDetail.row_count?.toLocaleString()} 行 {tableDetail.engine} {tableDetail.charset} )}
{tableDetail?.comment && (

{tableDetail.comment}

)}
{/* Tabs */}
{tabs.map(tab => { const isActive = activeTab === tab.key; const Icon = tab.icon; return ( ); })}
{/* Tab content */} {loading ? (
加载中...
) : activeTab === 'columns' ? ( /* Columns tab */
{(tableDetail?.columns || []).map(col => { const { icon: TypeIcon, color: typeColor } = getTypeIcon(col.Type); return ( ); })} {(!tableDetail || !tableDetail.columns || tableDetail.columns.length === 0) && ( )}
字段 类型 可空 键 默认值 额外 注释
{col.Field} {col.Type} {col.Null === 'YES' ? 'YES' : 'NO'} {col.Key ? ( {col.Key} ) : ( - )} {col.Default !== null ? col.Default : ( NULL )} {col.Extra || '-'} {col.Comment || '-'}
暂无字段信息
) : activeTab === 'indexes' ? ( /* Indexes tab */
{(tableDetail?.indexes || []).map((idx, i) => { const cols = idx.columns ? (Array.isArray(idx.columns) ? idx.columns.join(', ') : idx.columns) : '-'; const idxType = idx.index_type || (idx.unique !== undefined ? (idx.unique ? 'UNIQUE' : 'NORMAL') : '-'); return ( ); })} {(!tableDetail || !tableDetail.indexes || tableDetail.indexes.length === 0) && ( )}
索引名 字段 类型 定义
{idx.name} {cols} {idxType} {idx.definition || '-'}
暂无索引信息
) : activeTab === 'relations' ? ( /* Relations tab */ relations.length === 0 ? (

暂无外键关联信息

) : (
{relations.map((rel, i) => (
{rel.from_table} . {rel.from_column}
{rel.constraint_name}
{rel.to_table} . {rel.to_column}
外键引用
))}
) ) : ( /* DDL tab with syntax highlighting */
CREATE TABLE 语句
{tableDetail?.create_ddl ? (
                      ) : (
                        
                          -- 暂无DDL信息
                        
)}
)}
)}
); }