Implement DataTable component for improved table rendering and add export functionality in QueryEditor

This commit is contained in:
Ethanfly 2025-12-29 18:36:51 +08:00
parent 96be70c976
commit ebbbe46d22
2 changed files with 156 additions and 63 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -383,6 +383,10 @@ function TableViewer({ tab, onLoadPage }: {
<span className="text-white/40 text-sm">({tab.total} )</span> <span className="text-white/40 text-sm">({tab.total} )</span>
</div> </div>
<span className="text-xs text-white/30 flex items-center gap-1">
<Pin size={12} />
</span>
{/* 分页控制 */} {/* 分页控制 */}
<div className="flex items-center gap-2 ml-auto"> <div className="flex items-center gap-2 ml-auto">
<button <button
@ -405,69 +409,14 @@ function TableViewer({ tab, onLoadPage }: {
</div> </div>
</div> </div>
{/* 数据表格 - 使用绝对定位确保滚动 */} {/* 数据表格 - 使用 DataTable 组件支持列固定 */}
<div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}> <div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
<div style={{ <div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
position: 'absolute', <DataTable
top: 0, columns={tab.columns}
left: 0, data={tab.data}
right: 0, showColumnInfo={true}
bottom: 0, />
overflow: 'auto'
}}>
<table className="text-sm border-collapse" style={{ minWidth: 'max-content' }}>
<thead className="sticky top-0 z-10">
<tr>
{tab.columns.map((col, i) => (
<th
key={i}
className="px-4 py-2 text-left font-medium border-b border-metro-border whitespace-nowrap"
style={{ background: '#2d2d2d' }}
title={col.comment ? `${col.name}\n类型: ${col.type}\n备注: ${col.comment}` : `${col.name}\n类型: ${col.type}`}
>
<div className="flex items-center gap-1.5">
{col.key === 'PRI' && <Key size={12} className="text-accent-orange" />}
<span className="text-accent-blue">{col.name}</span>
<span className="text-white/30 font-normal text-xs">({col.type})</span>
{col.comment && (
<span className="text-accent-green text-xs" title={col.comment}>
<Info size={12} />
</span>
)}
</div>
{col.comment && (
<div className="text-xs text-white/40 font-normal mt-0.5 max-w-[200px] truncate">
{col.comment}
</div>
)}
</th>
))}
</tr>
</thead>
<tbody>
{tab.data.map((row, i) => (
<tr key={i} className="hover:bg-metro-surface/50">
{tab.columns.map((col, j) => (
<td key={j} className="px-4 py-1.5 border-b border-metro-border/50 font-mono text-white/80 whitespace-nowrap">
{row[col.name] === null ? (
<span className="text-white/30 italic">NULL</span>
) : typeof row[col.name] === 'object' ? (
<span className="text-accent-purple">{JSON.stringify(row[col.name])}</span>
) : (
String(row[col.name])
)}
</td>
))}
</tr>
))}
</tbody>
</table>
{tab.data.length === 0 && (
<div className="h-32 flex items-center justify-center text-white/30">
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@ -569,4 +518,147 @@ function QueryEditor({ tab, databases, tables, columns, onRun, onUpdateSql, onUp
sqlContent += `INSERT INTO \`${tableName}\` (\`${columns.join('`, `')}\`) VALUES (${values});\n` sqlContent += `INSERT INTO \`${tableName}\` (\`${columns.join('`, `')}\`) VALUES (${values});\n`
}) })
const blob = n const blob = new Blob([sqlContent], { type: 'text/plain;charset=utf-8' })
saveAs(blob, `query_results_${Date.now()}.sql`)
}
// 导出下拉菜单状态
const [showExportMenu, setShowExportMenu] = useState(false)
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* SQL 编辑区 */}
<div style={{ height: '200px', flexShrink: 0, display: 'flex', flexDirection: 'column', borderBottom: '1px solid #5d5d5d' }}>
<div className="h-10 bg-metro-bg flex items-center px-2 gap-2" style={{ flexShrink: 0 }}>
<button
onClick={handleRun}
className="h-7 px-4 bg-accent-green hover:bg-accent-green/90 flex items-center gap-1.5 text-sm transition-colors"
title="执行 SQL (Ctrl+Enter)"
>
<Play size={14} fill="currentColor" />
</button>
<div className="w-px h-5 bg-white/20 mx-1" />
<button
onClick={handleOpenFile}
className="h-7 px-3 bg-metro-surface hover:bg-metro-surface/80 flex items-center gap-1.5 text-sm transition-colors"
title="打开 SQL 文件 (Ctrl+O)"
>
<FolderOpen size={14} />
</button>
<button
onClick={handleSaveFile}
className="h-7 px-3 bg-metro-surface hover:bg-metro-surface/80 flex items-center gap-1.5 text-sm transition-colors"
title="保存 SQL 文件 (Ctrl+S)"
>
<Save size={14} />
</button>
<button
onClick={handleFormat}
className="h-7 px-3 bg-metro-surface hover:bg-metro-surface/80 flex items-center gap-1.5 text-sm transition-colors"
title="格式化 SQL (Ctrl+Shift+F)"
>
<AlignLeft size={14} />
</button>
<div className="w-px h-5 bg-white/20 mx-1" />
{/* 导出按钮 */}
<div className="relative">
<button
onClick={() => setShowExportMenu(!showExportMenu)}
className="h-7 px-3 bg-metro-surface hover:bg-metro-surface/80 flex items-center gap-1.5 text-sm transition-colors"
title="导出结果"
disabled={!tab.results || tab.results.rows.length === 0}
>
<Download size={14} />
</button>
{showExportMenu && (
<div className="absolute top-full left-0 mt-1 bg-metro-surface border border-metro-border rounded shadow-lg z-50 min-w-[140px]">
<button
onClick={() => { handleExportExcel(); setShowExportMenu(false) }}
className="w-full px-3 py-2 text-left text-sm hover:bg-accent-blue/20 flex items-center gap-2"
>
<FileSpreadsheet size={14} className="text-accent-green" />
Excel
</button>
<button
onClick={() => { handleExportSql(); setShowExportMenu(false) }}
className="w-full px-3 py-2 text-left text-sm hover:bg-accent-blue/20 flex items-center gap-2"
>
<FileCode size={14} className="text-accent-orange" />
SQL
</button>
</div>
)}
</div>
<span className="text-xs text-white/40 ml-auto">
{filePath && <span className="mr-3 text-accent-blue">{filePath.split(/[/\\]/).pop()}</span>}
Ctrl+Enter | Ctrl+S | Ctrl+Shift+F
</span>
</div>
<div style={{ flex: 1, minHeight: 0 }}>
<SqlEditor
value={sql}
onChange={setSql}
onRun={handleRun}
onSave={handleSaveFile}
onOpen={handleOpenFile}
onFormat={handleFormat}
databases={databases}
tables={tables}
columns={columns}
/>
</div>
</div>
{/* 结果区 - 使用 DataTable 组件支持列固定 */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div className="h-9 bg-metro-bg flex items-center px-3 border-b border-metro-border" style={{ flexShrink: 0 }}>
<span className="text-sm text-white/60">
{tab.results && <span className="ml-2 text-white/40">({tab.results.rows.length} )</span>}
</span>
{tab.results && tab.results.rows.length > 0 && (
<span className="text-xs text-white/30 ml-4 flex items-center gap-1">
<Pin size={12} />
</span>
)}
</div>
<div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
{tab.results ? (
<DataTable
columns={tab.results.columns.map(col => {
const colInfo = findColumnInfo(col)
return {
name: col,
type: colInfo?.type,
key: colInfo?.key,
comment: colInfo?.comment,
}
})}
data={tab.results.rows}
showColumnInfo={true}
/>
) : (
<div className="h-full flex items-center justify-center text-white/30">
</div>
)}
</div>
</div>
</div>
</div>
)
}