Files
opsxsql/frontend/src/pages/DataDictionaryPage.tsx
T
logan 8b921c6c83 refactor: mock清理+安全配置+通用组件+菜单优化
P0安全: SECRET_KEY/ADMIN_PASSWORD/CORS环境变量化, seed仅admin

P1Mock: mock.ts 301->88行, 删8数据mock+5generateMock, net-1300行

P2组件: useToast/useFetchList+ConfirmDialog/FormDialog

P3菜单: menuSections/menuItems->constants/menu.ts

P4API: NotificationsPage/WorkflowPage重写+RolesPage演示

测试: 1050passed, tsc clean
2026-05-24 02:42:29 +08:00

689 lines
30 KiB
TypeScript

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, '&lt;')
.replace(/>/g, '&gt;');
keywords.forEach(kw => {
const regex = new RegExp(`\\b${kw}\\b`, 'gi');
highlighted = highlighted.replace(regex, `<span class="text-purple-400 font-semibold">${kw}</span>`);
});
highlighted = highlighted.replace(/'[^']*'/g, '<span class="text-green-400">$&</span>');
highlighted = highlighted.replace(/`[^`]*`/g, '<span class="text-blue-400">$&</span>');
highlighted = highlighted.replace(/\b\d+\b/g, '<span class="text-orange-400">$&</span>');
highlighted = highlighted.replace(/(--.*$)/gm, '<span class="text-[var(--dg-text-muted)] italic">$1</span>');
highlighted = highlighted.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="text-[var(--dg-text-muted)] italic">$1</span>');
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 (
<div
onClick={onClick}
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-md text-sm cursor-pointer transition-colors ${
isActive ? 'bg-blue-500/15 text-blue-400 font-medium' : 'text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-tertiary)]'
}`}
style={{ paddingLeft: `${depth * 14 + 8}px` }}
>
{hasChildren && (
<button
onClick={(e) => { e.stopPropagation(); onToggle(); }}
className="flex-shrink-0 hover:opacity-70 transition-opacity"
>
<ChevronRight className={`w-3 h-3 text-[var(--dg-text-muted)] transition-transform ${expanded ? 'rotate-90' : ''}`} />
</button>
)}
{!hasChildren && <div className="w-3" />}
<Icon className={`w-3.5 h-3.5 flex-shrink-0 ${isActive ? 'text-blue-400' : 'text-[var(--dg-text-muted)]'}`} />
<span className="truncate flex-1 min-w-0">{label}</span>
{count !== undefined && <span className="text-xs text-[var(--dg-text-muted)] flex-shrink-0">{count}</span>}
</div>
);
}
/* ───────── Main Component ───────── */
export default function DataDictionaryPage() {
const [instanceId, setInstanceId] = useState<number | null>(null);
const [instances, setInstances] = useState<any[]>([]);
const [currentDb, setCurrentDb] = useState('');
const [databases, setDatabases] = useState<string[]>([]);
const [tables, setTables] = useState<string[]>([]);
const [searchFilter, setSearchFilter] = useState('');
const [selectedTable, setSelectedTable] = useState('');
const [tableDetail, setTableDetail] = useState<TableDetail | null>(null);
const [activeTab, setActiveTab] = useState<TabKey>('columns');
const [loading, setLoading] = useState(false);
const [expandedDbs, setExpandedDbs] = useState<Set<string>>(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 (
<div className="space-y-5">
{/* Page header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold flex items-center gap-2">
<BookOpen className="w-5 h-5 text-blue-400" />
数据字典
</h1>
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">
浏览数据库结构,查看表字段、索引、关系和DDL定义
</p>
</div>
<button
onClick={handleExport}
disabled={!instanceId || !currentDb}
className="flex items-center gap-2 px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Download className="w-4 h-4" />
导出字典
</button>
</div>
{/* Main layout */}
<div className="flex gap-5">
{/* Left panel */}
<div className="w-1/4 space-y-4">
{/* Instance selector */}
<Card className="dg-card">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Database className="w-4 h-4 text-blue-400" />
选择实例
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<select
value={instanceId || ''}
onChange={e => {
const val = e.target.value;
setInstanceId(val ? Number(val) : null);
setSelectedTable('');
setTableDetail(null);
}}
className="w-full px-3 py-2 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
>
<option value="">-- 请选择实例 --</option>
{instances.map(inst => (
<option key={inst.id} value={inst.id}>
{inst.name} ({inst.db_type})
</option>
))}
</select>
</CardContent>
</Card>
{/* Database tree */}
<Card className="dg-card">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<FolderOpen className="w-4 h-4 text-green-400" />
数据库
{databases.length > 0 && (
<span className="text-xs text-[var(--dg-text-secondary)] ml-auto">({databases.length})</span>
)}
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
{databases.length === 0 ? (
<div className="text-center py-4 text-sm text-[var(--dg-text-secondary)]">
{instanceId ? '暂无数据库' : '请先选择实例'}
</div>
) : (
<div className="space-y-0.5">
{databases.map(db => {
const isExpanded = expandedDbs.has(db);
const isActive = currentDb === db;
return (
<div key={db}>
<DbTreeNode
label={db}
isActive={isActive}
hasChildren={true}
expanded={isExpanded}
onClick={() => {
setCurrentDb(db);
if (!isExpanded) toggleDb(db);
}}
onToggle={() => toggleDb(db)}
depth={0}
icon={Database}
/>
{isExpanded && currentDb === db && (
<div className="mt-0.5">
{/* Table search */}
<div className="relative px-2 mb-1">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-3 h-3 text-[var(--dg-text-muted)]" />
<input
type="text"
value={searchFilter}
onChange={e => 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()}
/>
</div>
{filteredTables.length === 0 ? (
<div className="text-center py-2 text-xs text-[var(--dg-text-secondary)]">
没有匹配的表
</div>
) : (
filteredTables.map(tbl => (
<DbTreeNode
key={tbl}
label={tbl}
isActive={selectedTable === tbl}
hasChildren={false}
expanded={false}
onClick={() => setSelectedTable(tbl)}
onToggle={() => {}}
depth={1}
icon={Table2}
/>
))
)}
</div>
)}
</div>
);
})}
</div>
)}
</CardContent>
</Card>
</div>
{/* Right panel */}
<div className="w-3/4">
{!tableDetail && !loading ? (
<Card className="dg-card h-full">
<CardContent className="flex flex-col items-center justify-center py-20 text-[var(--dg-text-secondary)]">
<Table2 className="w-12 h-12 mb-3 opacity-30" />
<p className="text-sm">选择一个表以查看详细信息</p>
</CardContent>
</Card>
) : (
<Card className="dg-card">
{/* Table header */}
<CardHeader className="pb-3 border-b border-[var(--dg-border)]">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-wrap">
<CardTitle className="text-base font-semibold">{selectedTable}</CardTitle>
{tableDetail && (
<>
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-500/15 text-purple-400 flex items-center gap-1">
<Gauge className="w-3 h-3" />
{tableDetail.table_size}
</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-blue-500/15 text-blue-400">
<Hash className="w-3 h-3 inline mr-1" />
{tableDetail.row_count?.toLocaleString()} 行
</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-green-500/15 text-green-400">
{tableDetail.engine}
</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-[var(--dg-text-muted)]/15 text-[var(--dg-text-muted)]">
{tableDetail.charset}
</span>
</>
)}
</div>
<button
onClick={() => {
if (instanceId && currentDb && selectedTable) {
setLoading(true);
dictionaryApi.getTableDetail(instanceId, selectedTable, currentDb)
.then((data: any) => setTableDetail(data || null))
.catch(() => setTableDetail(null))
.finally(() => setLoading(false));
}
}}
className="p-1.5 rounded-md hover:bg-[var(--dg-bg-tertiary)] transition-colors"
title="刷新"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin text-blue-400' : ''}`} />
</button>
</div>
{tableDetail?.comment && (
<p className="text-xs text-[var(--dg-text-secondary)] mt-1.5">{tableDetail.comment}</p>
)}
</CardHeader>
{/* Tabs */}
<div className="flex border-b border-[var(--dg-border)] px-5">
{tabs.map(tab => {
const isActive = activeTab === tab.key;
const Icon = tab.icon;
return (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
isActive
? 'border-blue-500 text-blue-400'
: 'border-transparent text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]'
}`}
>
<Icon className="w-4 h-4" />
{tab.label}
</button>
);
})}
</div>
{/* Tab content */}
<CardContent className="p-5">
{loading ? (
<div className="flex items-center justify-center py-10">
<RefreshCw className="w-6 h-6 animate-spin text-blue-400" />
<span className="ml-2 text-sm text-[var(--dg-text-secondary)]">加载中...</span>
</div>
) : activeTab === 'columns' ? (
/* Columns tab */
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
<th className="text-left px-4 py-2.5 font-semibold">字段</th>
<th className="text-left px-4 py-2.5 font-semibold">类型</th>
<th className="text-left px-4 py-2.5 font-semibold">可空</th>
<th className="text-left px-4 py-2.5 font-semibold">键</th>
<th className="text-left px-4 py-2.5 font-semibold">默认值</th>
<th className="text-left px-4 py-2.5 font-semibold">额外</th>
<th className="text-left px-4 py-2.5 font-semibold">注释</th>
</tr>
</thead>
<tbody>
{(tableDetail?.columns || []).map(col => {
const { icon: TypeIcon, color: typeColor } = getTypeIcon(col.Type);
return (
<tr key={col.Field} className="border-b border-[var(--dg-border-light)] hover:bg-[var(--dg-bg-hover)] transition-colors">
<td className="px-4 py-2.5 font-mono text-blue-400 flex items-center gap-1.5">
<TypeIcon className={`w-3.5 h-3.5 ${typeColor}`} />
{col.Field}
</td>
<td className="px-4 py-2.5 font-mono text-green-400 text-xs">{col.Type}</td>
<td className="px-4 py-2.5">
<span className={`text-xs px-1.5 py-0.5 rounded ${
col.Null === 'YES' ? 'bg-yellow-500/15 text-yellow-400' : 'bg-[var(--dg-text-muted)]/15 text-[var(--dg-text-muted)]'
}`}>
{col.Null === 'YES' ? 'YES' : 'NO'}
</span>
</td>
<td className="px-4 py-2.5">
{col.Key ? (
<span className="text-xs px-1.5 py-0.5 rounded bg-purple-500/15 text-purple-400 font-medium">
{col.Key}
</span>
) : (
<span className="text-[var(--dg-text-secondary)]">-</span>
)}
</td>
<td className="px-4 py-2.5 font-mono text-[var(--dg-text-secondary)] text-xs">
{col.Default !== null ? col.Default : (
<span className="text-[var(--dg-text-muted)]">NULL</span>
)}
</td>
<td className="px-4 py-2.5 text-[var(--dg-text-secondary)] text-xs">{col.Extra || '-'}</td>
<td className="px-4 py-2.5 text-[var(--dg-text-secondary)] text-xs max-w-[200px] truncate" title={col.Comment}>
{col.Comment || '-'}
</td>
</tr>
);
})}
{(!tableDetail || !tableDetail.columns || tableDetail.columns.length === 0) && (
<tr>
<td colSpan={7} className="text-center py-6 text-[var(--dg-text-secondary)]">
<Columns3 className="w-8 h-8 mx-auto mb-2 opacity-30" />
暂无字段信息
</td>
</tr>
)}
</tbody>
</table>
</div>
) : activeTab === 'indexes' ? (
/* Indexes tab */
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
<th className="text-left px-4 py-2.5 font-semibold">索引名</th>
<th className="text-left px-4 py-2.5 font-semibold">字段</th>
<th className="text-left px-4 py-2.5 font-semibold">类型</th>
<th className="text-left px-4 py-2.5 font-semibold">定义</th>
</tr>
</thead>
<tbody>
{(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 (
<tr key={idx.name || i} className="border-b border-[var(--dg-border-light)] hover:bg-[var(--dg-bg-hover)] transition-colors">
<td className="px-4 py-2.5 font-mono text-blue-400">{idx.name}</td>
<td className="px-4 py-2.5 font-mono text-xs">{cols}</td>
<td className="px-4 py-2.5">
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${
idxType === 'PRIMARY' || idxType === 'UNIQUE'
? 'bg-purple-500/15 text-purple-400'
: 'bg-green-500/15 text-green-400'
}`}>
{idxType}
</span>
</td>
<td className="px-4 py-2.5 text-[var(--dg-text-secondary)] text-xs font-mono max-w-xs truncate" title={idx.definition}>
{idx.definition || '-'}
</td>
</tr>
);
})}
{(!tableDetail || !tableDetail.indexes || tableDetail.indexes.length === 0) && (
<tr>
<td colSpan={4} className="text-center py-6 text-[var(--dg-text-secondary)]">
<Key className="w-8 h-8 mx-auto mb-2 opacity-30" />
暂无索引信息
</td>
</tr>
)}
</tbody>
</table>
</div>
) : activeTab === 'relations' ? (
/* Relations tab */
relations.length === 0 ? (
<div className="text-center py-10 text-[var(--dg-text-secondary)]">
<ArrowRight className="w-8 h-8 mx-auto mb-2 opacity-30" />
<p className="text-sm">暂无外键关联信息</p>
</div>
) : (
<div className="space-y-3">
{relations.map((rel, i) => (
<div key={i} className="flex items-center gap-3 p-3 bg-[var(--dg-bg-tertiary)] rounded-lg">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 text-sm">
<TableProperties className="w-4 h-4 text-blue-400" />
<span className="font-medium">{rel.from_table}</span>
<span className="text-[var(--dg-text-muted)]">.</span>
<span className="font-mono text-blue-400">{rel.from_column}</span>
</div>
<div className="text-xs text-[var(--dg-text-muted)] mt-0.5">{rel.constraint_name}</div>
</div>
<div className="flex items-center gap-1 text-[var(--dg-text-muted)]">
<ArrowRight className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 text-sm">
<TableProperties className="w-4 h-4 text-green-400" />
<span className="font-medium">{rel.to_table}</span>
<span className="text-[var(--dg-text-muted)]">.</span>
<span className="font-mono text-green-400">{rel.to_column}</span>
</div>
<div className="text-xs text-[var(--dg-text-muted)] mt-0.5">外键引用</div>
</div>
</div>
))}
</div>
)
) : (
/* DDL tab with syntax highlighting */
<div>
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-[var(--dg-text-muted)] flex items-center gap-1">
<Code2 className="w-3.5 h-3.5" />
CREATE TABLE 语句
</span>
<button
onClick={() => {
if (tableDetail?.create_ddl) {
navigator.clipboard.writeText(tableDetail.create_ddl);
}
}}
className="text-xs text-blue-400 hover:text-blue-300 transition-colors"
>
复制
</button>
</div>
<div className="rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] p-4 overflow-x-auto">
{tableDetail?.create_ddl ? (
<pre
className="text-sm font-mono whitespace-pre-wrap leading-relaxed"
dangerouslySetInnerHTML={{ __html: highlightDDL(tableDetail.create_ddl) }}
/>
) : (
<pre className="text-sm font-mono whitespace-pre-wrap text-[var(--dg-text-muted)]">
-- 暂无DDL信息
</pre>
)}
</div>
</div>
)}
</CardContent>
</Card>
)}
</div>
</div>
</div>
);
}