"use client" import * as React from "react" import {useCallback, useEffect, useRef, useState} from "react" import {useTheme} from "next-themes" import CodeMirror from "@uiw/react-codemirror" import {sql} from "@codemirror/lang-sql" import {toast} from "sonner" import { Activity, ArrowLeft, Cpu, Database, Download, FileText, HardDrive, Layers, Play, RefreshCw, Server, Terminal, Trash2, } from "lucide-react" import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" import {Button} from "@/components/ui/button" import {Skeleton} from "@/components/ui/skeleton" import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select" import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table" import {Input} from "@/components/ui/input" import {Switch} from "@/components/ui/switch" import {Label} from "@/components/ui/label" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import type {CacheStatus, DBOverview, ExecuteSQLResponse, TableDataResponse} from "@/lib/services" import services, {AdminService} from "@/lib/services" /** * 格式化数字,每3位加逗号 */ const formatNumber = (num: number | string) => { if (num === undefined || num === null) return "0" return num.toString().replace(/\B(?=(\d{3})+(?!\B))/g, ",") } /** * 格式化表格单元格内容 */ const formatCellValue = (val: unknown): string => { if (val === null || val === undefined) return "-" if (typeof val === "boolean") return val ? "true" : "false" if (typeof val === "object") { try { return JSON.stringify(val) } catch { return "[Object]" } } return String(val) } export function DatabaseManager() { // 基础状态 const [overview, setOverview] = useState(null) const [tables, setTables] = useState([]) const [selectedTable, setSelectedTable] = useState("") const [tableData, setTableData] = useState(null) // 分页状态 const [page, setPage] = useState(1) const pageSize = 10 // 加载状态 const [loadingOverview, setLoadingOverview] = useState(true) const [loadingTables, setLoadingTables] = useState(true) const [loadingData, setLoadingData] = useState(false) const [exporting, setExporting] = useState(false) // SQL 控制台状态 const [showConsole, setShowConsole] = useState(false) const [sqlQuery, setSqlQuery] = useState("") const [executingSQL, setExecutingSQL] = useState(false) const [sqlResult, setSqlResult] = useState(null) const [sqlError, setSqlError] = useState(null) // 缓存管理状态 const [cacheStatus, setCacheStatus] = useState(null) const [loadingCache, setLoadingCache] = useState(false) const [savingConfig, setSavingConfig] = useState(false) const [clearingCache, setClearingCache] = useState(false) const [maxSizeMB, setMaxSizeMB] = useState("100") const [ttlMinutes, setTtlMinutes] = useState("60") const [lruEnabled, setLruEnabled] = useState(true) const [showClearConfirm, setShowClearConfirm] = useState(false) // 拖拽与主题状态 const containerRef = useRef(null) const [editorHeight, setEditorHeight] = useState(240) const { resolvedTheme } = useTheme() const cmTheme = resolvedTheme === "dark" ? "dark" : "light" const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault() const startY = e.clientY const startHeight = editorHeight const handleMouseMove = (moveEvent: MouseEvent) => { if (!containerRef.current) return const deltaY = moveEvent.clientY - startY const containerHeight = containerRef.current.getBoundingClientRect().height const newHeight = startHeight + deltaY // Limit editor height between 80px and containerHeight - 80px if (newHeight > 80 && newHeight < containerHeight - 80) { setEditorHeight(newHeight) } } const handleMouseUp = () => { document.removeEventListener("mousemove", handleMouseMove) document.removeEventListener("mouseup", handleMouseUp) } document.addEventListener("mousemove", handleMouseMove) document.addEventListener("mouseup", handleMouseUp) } // 1. 获取运行概览 const fetchOverview = useCallback(async (isSilent = false) => { if (!isSilent) setLoadingOverview(true) try { const data = await services.dbManage.getOverview() setOverview(data) } catch (err) { toast.error("获取数据库概览失败", { description: err instanceof Error ? err.message : "未知错误", }) } finally { setLoadingOverview(false) } }, []) // 2. 获取表列表 const fetchTables = useCallback(async () => { setLoadingTables(true) try { const data = await services.dbManage.listTables() setTables(data) // 默认选择第一张表 if (data.length > 0 && !selectedTable) { setSelectedTable(data[0]) } } catch (err) { toast.error("获取数据库数据表列表失败", { description: err instanceof Error ? err.message : "未知错误", }) } finally { setLoadingTables(false) } }, [selectedTable]) // 3. 获取具体表的数据 const fetchTableData = useCallback(async (tableName: string, targetPage: number, size: number) => { if (!tableName) return setLoadingData(true) try { const data = await services.dbManage.getTableData({ table: tableName, page: targetPage, pageSize: size, }) setTableData(data) } catch (err) { toast.error(`获取数据表 ${tableName} 数据失败`, { description: err instanceof Error ? err.message : "未知错误", }) } finally { setLoadingData(false) } }, []) // 4. 导出数据库 const handleExport = async () => { setExporting(true) try { const { blob, filename } = await AdminService.exportDatabase() const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = filename document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) toast.success("数据库导出成功", { description: `已下载归档文件: ${filename}` }) } catch (err) { toast.error("数据库导出失败", { description: err instanceof Error ? err.message : "导出异常", }) } finally { setExporting(false) } } // 5. 执行 SQL 查询 const handleExecuteSQL = async () => { if (!sqlQuery.trim()) return setExecutingSQL(true) setSqlResult(null) setSqlError(null) try { const result = await services.dbManage.executeSQL(sqlQuery) setSqlResult(result) toast.success("SQL 执行成功") } catch (err) { setSqlError(err instanceof Error ? err.message : "未知执行错误") toast.error("SQL 执行失败") } finally { setExecutingSQL(false) } } // 6. 格式化字节大小 const formatBytes = (bytes: number) => { if (bytes === 0) return "0 Bytes" const k = 1024 const sizes = ["Bytes", "KB", "MB", "GB", "TB"] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] } // 7. 获取磁盘缓存状态 const fetchCacheStatus = useCallback(async (isSilent = false) => { if (!isSilent) setLoadingCache(true) try { const data = await AdminService.getCacheStatus() setCacheStatus(data) setMaxSizeMB(data.max_size_mb.toString()) setTtlMinutes(data.ttl_minutes.toString()) setLruEnabled(data.lru_enabled) } catch (err) { toast.error("获取磁盘缓存状态失败", { description: err instanceof Error ? err.message : "未知错误", }) } finally { setLoadingCache(false) } }, []) // 8. 保存磁盘缓存配置 const handleSaveConfig = async (e: React.FormEvent) => { e.preventDefault() const size = parseInt(maxSizeMB, 10) const ttl = parseInt(ttlMinutes, 10) if (isNaN(size) || size < 1) { toast.error("保存失败", { description: "最大容量限制必须是大于等于 1 的整数" }) return } if (isNaN(ttl) || ttl < 0) { toast.error("保存失败", { description: "默认过期时间必须是大于等于 0 的整数" }) return } setSavingConfig(true) try { await AdminService.updateCacheConfig({ max_size_mb: size, ttl_minutes: ttl, lru_enabled: lruEnabled, }) toast.success("保存成功", { description: "磁盘缓存策略已热更新" }) await fetchCacheStatus(true) } catch (err) { toast.error("保存配置失败", { description: err instanceof Error ? err.message : "未知错误", }) } finally { setSavingConfig(false) } } // 9. 清空磁盘缓存数据 const handleClearCache = async () => { setClearingCache(true) try { await AdminService.clearCache() toast.success("清空成功", { description: "缓存数据已全部清除" }) setShowClearConfirm(false) await fetchCacheStatus(true) } catch (err) { toast.error("清空缓存失败", { description: err instanceof Error ? err.message : "未知错误", }) } finally { setClearingCache(false) } } // 页面初始化加载 useEffect(() => { fetchOverview() fetchTables() fetchCacheStatus() }, [fetchOverview, fetchTables, fetchCacheStatus]) // 选择数据表或页码变化时,拉取对应的数据 useEffect(() => { if (selectedTable) { fetchTableData(selectedTable, page, pageSize) } }, [selectedTable, page, pageSize, fetchTableData]) // 处理页码改变 const handlePageChange = (newPage: number) => { setPage(newPage) } // 处理表切换 const handleTableChange = (value: string) => { setSelectedTable(value) setPage(1) // 切换表重置为第一页 } // Preset SQL 选择器 const handlePresetSQLChange = (query: string) => { setSqlQuery(query) } // 渲染概览骨架 const renderOverviewSkeleton = () => (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) // SQL 查询控台渲染 if (showConsole) { return (
{/* 顶部控制与标题 */}

SQL 查询终端

执行自定义 SQL 查询,支持数据读取与写入操作

{overview?.type === "postgres" ? "PostgreSQL Connected" : "SQLite Connected"}
{/* 类似于 VS Code 的单个整体编辑器+结果区域,固定高度,有分水岭拖拽调整大小 */}
{/* 顶部编辑工具栏 */}
SQL 编辑器
{/* 快速模板 */}
快速模板:
{/* 控制按钮 */}
{/* 上半区:编辑器输入框 */}
setSqlQuery(value)} className="h-full text-xs font-mono" basicSetup={{ lineNumbers: true, foldGutter: true, dropCursor: true, allowMultipleSelections: false, indentOnInput: true, }} />
{/* 拖动分割线 */}
{/* 下半区:查看结果 (可滑动查看) */}
{/* 结果栏工具提示 */}
执行输出 {sqlResult && ( 类型: {sqlResult.type.toUpperCase()} | 耗时: {sqlResult.execution_time_ms} ms )}
{/* 滚动结果内容 */}
{executingSQL && (
正在在数据库执行查询,请稍候...
)} {sqlError && (

SQL 执行报错 (Error):

{sqlError}
)} {sqlResult && (
{sqlResult.type === "select" && sqlResult.columns && sqlResult.columns.length > 0 && (
{sqlResult.columns.map((col) => ( {col} ))} {sqlResult.results && sqlResult.results.length > 0 ? ( sqlResult.results.map((row, rIndex) => ( {sqlResult.columns!.map((col) => ( {formatCellValue(row[col])} ))} )) ) : ( 查询结果为空 )}
)} {sqlResult.type === "exec" && (

SQL 执行成功

受影响行数: {sqlResult.affected_rows} 行,耗时 {sqlResult.execution_time_ms} 毫秒。

)}
)} {!executingSQL && !sqlResult && !sqlError && (
编辑器就绪,请在上方编写 SQL 并运行查看结果
)}
) } // 正常数据概览及表分页浏览器渲染 return (
{/* 顶部控制与标题 */}

数据管理

查看物理数据库运行运维数据,进行表级数据浏览与 SQL 交互查询

{/* 1. 顶部概览卡片 */} {loadingOverview ? ( renderOverviewSkeleton() ) : ( overview && (
{/* 卡片1: 数据库类型 */} 数据库类型
{overview.type}
{/* 卡片2: 数据库版本 */} 版本信息
{overview.version.split(" ").slice(0, 2).join(" ")}
{/* 卡片3: 数据库名称 */} 名称/路径
{overview.name.substring(overview.name.lastIndexOf("/") + 1)}
{/* 卡片4: 数据库大小 */} 数据库大小
{overview.size}
{/* 卡片5: 表数量 */} 物理数据表
{formatNumber(overview.table_count)}
{/* 卡片6: 连接数 */} 活跃连接数
{formatNumber(overview.connections)}
) )} {/* 2. 中部数据表浏览器 */}
数据表浏览器 浏览数据库中的物理数据表详情及内容
{loadingTables ? ( ) : ( )}
{loadingData && !tableData ? (
) : tableData && tableData.columns.length > 0 ? (
{/* 数据表格区域 */}
{loadingData && (
)} {tableData.columns.map((col) => ( {col} ))} {tableData.results && tableData.results.length > 0 ? ( tableData.results.map((row, rIndex) => ( {tableData.columns.map((col) => ( {formatCellValue(row[col])} ))} )) ) : ( 表中无记录数据 )}
{/* 分页控制 */}
总行数: {formatNumber(tableData.total)} 条记录
第 {page} / {Math.max(1, Math.ceil(tableData.total / pageSize))} 页
) : (
未选择数据表或表结构无法加载
)}
{/* 2.5 缓存管理 */}
缓存管理
管理和监控系统级磁盘缓存的资源占用、生命周期及淘汰策略
{loadingCache && !cacheStatus ? (
) : cacheStatus ? (
{/* 左边:状态区 (2/5 cols) */}

运行状态

{/* 已占空间 */}

已用空间

{formatBytes(cacheStatus.total_size)}

{/* Key数量 */}

缓存键数量

{formatNumber(cacheStatus.keys_count)} 个文件

{/* 存储路径 */}

缓存基准目录

{cacheStatus.base_path}
{/* 右边:配置区 (3/5 cols) */}

策略配置

setMaxSizeMB(e.target.value)} className="h-8 text-xs bg-background/50 border-border/40" placeholder="例如 100" required />

当总大小超出该值时,自动触发淘汰算法

setTtlMinutes(e.target.value)} className="h-8 text-xs bg-background/50 border-border/40" placeholder="例如 60,0 表示永不过期" required />

缓存项的最长存活时间,超期后将失效被清理

在到达最大容量限制时,自动移除最久未被访问的缓存项。关闭该功能仅清理过期项。
) : (
未加载到缓存状态信息
)}
{/* 2.6 清除缓存确认对话框 */} 确认清空所有缓存? 该操作将彻底清空磁盘目录下的全部缓存文件(包含临时解压、处理后的图片及各种块文件),重置键数量统计为 0。此操作不可撤销,且可能导致用户拉取资源时出现一过性的响应变慢。 {/* 3. 底部功能区 */} 功能区 数据库导出及自定义高级 SQL 执行终端
{/* 功能一: 数据库导出 */}

数据库备份导出

直接导出并下载物理数据库镜像文件(SQLite 导出 .db,PostgreSQL 导出为打包的 .sql 文本文件)

{/* 功能二: SQL 查询终端 */}

SQL 查询控台

打开在线 SQL 控制终端,执行原生的 SQL 进行数据筛选、更新、调试或性能优化

) }