/** * 数据库查看器页面。 * * - 左侧:Schema → Table 层级树,异步加载 * - 右侧上方:SQL 编辑器 + 执行按钮 * - 右侧下方:列定义 / 查询结果 Table */ import React, { useEffect, useState, useCallback } from 'react'; import { Tree, Input, Button, Table, Space, message, Spin, Tag, Card, Typography, Tooltip } from 'antd'; import { PlayCircleOutlined, ReloadOutlined, TableOutlined, DatabaseOutlined, CopyOutlined, } from '@ant-design/icons'; import type { DataNode, EventDataNode } from 'antd/es/tree'; import type { ColumnsType } from 'antd/es/table'; import { fetchSchemas, fetchTables, fetchColumns, executeQuery, type ColumnInfo, type QueryResult, } from '../api/dbViewer'; const { TextArea } = Input; const { Title, Text } = Typography; const schemaKey = (schema: string) => `s::${schema}`; const tableKey = (schema: string, table: string) => `t::${schema}::${table}`; function parseTableKey(key: string): { schema: string; table: string } | null { if (!key.startsWith('t::')) return null; const parts = key.slice(3).split('::'); if (parts.length !== 2) return null; return { schema: parts[0], table: parts[1] }; } const DBViewer: React.FC = () => { const [treeData, setTreeData] = useState([]); const [loadingTree, setLoadingTree] = useState(false); const [expandedKeys, setExpandedKeys] = useState([]); const [selectedTable, setSelectedTable] = useState<{ schema: string; table: string } | null>(null); const [columnData, setColumnData] = useState([]); const [loadingColumns, setLoadingColumns] = useState(false); const [sql, setSql] = useState(''); const [queryResult, setQueryResult] = useState(null); const [loadingQuery, setLoadingQuery] = useState(false); const loadSchemas = useCallback(async () => { setLoadingTree(true); try { const schemas = await fetchSchemas(); setTreeData( schemas.map((s) => ({ title: s, key: schemaKey(s), icon: , isLeaf: false, })), ); } catch { message.error('加载 Schema 列表失败'); } finally { setLoadingTree(false); } }, []); useEffect(() => { loadSchemas(); }, [loadSchemas]); const onLoadData = async (node: EventDataNode) => { const key = node.key as string; if (!key.startsWith('s::')) return; if (node.children && node.children.length > 0) return; const schema = key.slice(3); try { const tables = await fetchTables(schema); const children: DataNode[] = tables.map((t) => ({ title: ( {t.name} ({t.row_count.toLocaleString()}) ), key: tableKey(schema, t.name), icon: , isLeaf: true, })); setTreeData((prev) => prev.map((n) => n.key === key ? { ...n, children } : n)); } catch { message.error(`加载 ${schema} 的表列表失败`); } }; const onSelectNode = async (_: React.Key[], info: { node: DataNode }) => { const key = info.node.key as string; const parsed = parseTableKey(key); if (!parsed) return; setSelectedTable(parsed); setLoadingColumns(true); setQueryResult(null); try { const cols = await fetchColumns(parsed.schema, parsed.table); setColumnData(cols); setSql(`SELECT * FROM ${parsed.schema}.${parsed.table} LIMIT 100;`); } catch { message.error('加载列定义失败'); setColumnData([]); } finally { setLoadingColumns(false); } }; const handleExecute = async () => { const trimmed = sql.trim(); if (!trimmed) { message.warning('请输入 SQL 语句'); return; } setLoadingQuery(true); try { const result = await executeQuery(trimmed); setQueryResult(result); } catch (err: unknown) { const axiosErr = err as { response?: { data?: { detail?: string } } }; const msg = axiosErr.response?.data?.detail ?? (err instanceof Error ? err.message : '查询执行失败'); message.error(msg); setQueryResult(null); } finally { setLoadingQuery(false); } }; const handleCopySql = () => { navigator.clipboard.writeText(sql).then(() => message.success('已复制')); }; const columnDefColumns: ColumnsType = [ { title: '列名', dataIndex: 'name', key: 'name', render: (v: string) => {v} }, { title: '数据类型', dataIndex: 'data_type', key: 'data_type' }, { title: '可空', dataIndex: 'is_nullable', key: 'is_nullable', width: 70, align: 'center', render: (v: boolean) => v ? YES : NO, }, { title: '默认值', dataIndex: 'default_value', key: 'default_value', render: (v: string | null) => v != null ? {v} : , }, ]; const resultColumns: ColumnsType> = queryResult ? queryResult.columns.map((col, idx) => ({ title: col, dataIndex: String(idx), key: col, ellipsis: true, render: (v: unknown) => { if (v === null || v === undefined) return NULL; return String(v); }, })) : []; const resultDataSource: Record[] = queryResult ? queryResult.rows.map((row, rowIdx) => { const obj: Record = { _key: rowIdx }; row.forEach((cell, colIdx) => { obj[String(colIdx)] = cell; }); return obj; }) : []; return (
<DatabaseOutlined style={{ marginRight: 8 }} /> 数据库查看器 {selectedTable && ( {selectedTable.schema}.{selectedTable.table} )}
{/* 左侧树 */} } onClick={loadSchemas} loading={loadingTree} />} style={{ width: 260, minWidth: 260, display: 'flex', flexDirection: 'column' }} styles={{ body: { flex: 1, overflow: 'auto', padding: '8px 12px' } }} > setExpandedKeys(keys)} onSelect={onSelectNode} /> {/* 右侧 */}
{/* SQL 编辑器 */}