fix(frontend): keep browser API on same-origin rewrites in dev

Document that NEXT_PUBLIC_WAVELET_BACKEND_URL must stay unset for local
next dev so axios hits /api/* and Next rewrites proxy to the backend.
This commit is contained in:
ryan
2026-07-24 16:37:24 +08:00
parent a4588b05e0
commit 9f15f42b47
75 changed files with 5971 additions and 2450 deletions
@@ -4,6 +4,7 @@ import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'sonner';
import { HardDrive, RefreshCw, Trash2 } from 'lucide-react';
import { useTranslations } from 'next-intl';
import {
Card,
@@ -52,6 +53,7 @@ interface CacheManagerProps {
}
export function CacheManager({ refreshTrigger }: CacheManagerProps) {
const t = useTranslations('admin.database.cache');
const [cacheStatus, setCacheStatus] = useState<CacheStatus | null>(null);
const [loadingCache, setLoadingCache] = useState<boolean>(true);
const [savingConfig, setSavingConfig] = useState<boolean>(false);
@@ -64,22 +66,25 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
const [lruEnabled, setLruEnabled] = useState<boolean>(true);
// 获取磁盘缓存状态
const fetchCacheStatus = useCallback(async (isSilent = false) => {
if (!isSilent) setLoadingCache(true);
try {
const data = await services.adminCache.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);
}
}, []);
const fetchCacheStatus = useCallback(
async (isSilent = false) => {
if (!isSilent) setLoadingCache(true);
try {
const data = await services.adminCache.getCacheStatus();
setCacheStatus(data);
setMaxSizeMB(data.max_size_mb.toString());
setTtlMinutes(data.ttl_minutes.toString());
setLruEnabled(data.lru_enabled);
} catch (err) {
toast.error(t('fetchCacheStatusFailed'), {
description: err instanceof Error ? err.message : '未知错误',
});
} finally {
setLoadingCache(false);
}
},
[t],
);
// 保存配置
const handleSaveConfig = async (e: React.FormEvent) => {
@@ -87,14 +92,14 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
const size = parseInt(maxSizeMB, 10);
const ttl = parseInt(ttlMinutes, 10);
if (isNaN(size) || size < 1) {
toast.error('保存失败', {
description: '最大容量限制必须是大于等于 1 的整数',
toast.error(t('saveFailed'), {
description: t('maxSizeMustBePositive'),
});
return;
}
if (isNaN(ttl) || ttl < 0) {
toast.error('保存失败', {
description: '默认过期时间必须是大于等于 0 的整数',
toast.error(t('saveFailed'), {
description: t('ttlMustBeNonNegative'),
});
return;
}
@@ -105,10 +110,12 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
ttl_minutes: ttl,
lru_enabled: lruEnabled,
});
toast.success('保存成功', { description: '磁盘缓存策略已热更新' });
toast.success(t('saveSuccess'), {
description: t('cachePolicyHotUpdated'),
});
await fetchCacheStatus(true);
} catch (err) {
toast.error('保存配置失败', {
toast.error(t('saveConfigFailed'), {
description: err instanceof Error ? err.message : '未知错误',
});
} finally {
@@ -121,11 +128,11 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
setClearingCache(true);
try {
await services.adminCache.clearCache();
toast.success('清空成功', { description: '缓存数据已全部清除' });
toast.success(t('clearSuccess'), { description: t('cacheDataCleared') });
setShowClearConfirm(false);
await fetchCacheStatus(true);
} catch (err) {
toast.error('清空缓存失败', {
toast.error(t('clearCacheFailed'), {
description: err instanceof Error ? err.message : '未知错误',
});
} finally {
@@ -149,10 +156,12 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
<div className='space-y-0.5'>
<div className='flex items-center gap-2'>
<HardDrive className='size-4 text-primary animate-pulse' />
<CardTitle className='text-sm font-semibold'>缓存管理</CardTitle>
<CardTitle className='text-sm font-semibold'>
{t('cacheManagement')}
</CardTitle>
</div>
<CardDescription className='text-[11px]'>
管理和监控系统级磁盘缓存的资源占用、生命周期及淘汰策略
{t('cacheManagementDesc')}
</CardDescription>
</div>
@@ -166,7 +175,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
<RefreshCw
className={`size-3 ${loadingCache ? 'animate-spin' : ''}`}
/>
刷新状态
{t('refreshStatus')}
</Button>
</CardHeader>
<CardContent className='pt-4'>
@@ -181,13 +190,13 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
{/* 左边:状态区 (2/5 cols) */}
<div className='lg:col-span-2 space-y-4'>
<h4 className='text-xs font-semibold text-muted-foreground uppercase tracking-wider'>
运行状态
{t('runtimeStatus')}
</h4>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
{/* 已占空间 */}
<div className='p-4 rounded-xl border border-border/40 bg-background/30 backdrop-blur-xs hover:border-primary/20 transition-all duration-300'>
<p className='text-[10px] text-muted-foreground font-medium mb-1'>
已用空间
{t('usedSpace')}
</p>
<p className='text-xl font-bold tracking-tight text-foreground'>
{formatBytes(cacheStatus.total_size)}
@@ -196,12 +205,12 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
{/* Key数量 */}
<div className='p-4 rounded-xl border border-border/40 bg-background/30 backdrop-blur-xs hover:border-primary/20 transition-all duration-300'>
<p className='text-[10px] text-muted-foreground font-medium mb-1'>
缓存键数量
{t('cacheKeysCount')}
</p>
<p className='text-xl font-bold tracking-tight text-foreground'>
{formatNumber(cacheStatus.keys_count)}{' '}
<span className='text-xs text-muted-foreground font-normal'>
个文件
{t('files')}
</span>
</p>
</div>
@@ -210,7 +219,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
{/* 存储路径 */}
<div className='p-4 rounded-xl border border-border/40 bg-background/30 backdrop-blur-xs hover:border-primary/20 transition-all duration-300'>
<p className='text-[10px] text-muted-foreground font-medium mb-1.5'>
缓存基准目录
{t('cacheBaseDirectory')}
</p>
<code
className='text-xs font-mono bg-muted/60 px-2 py-1 rounded-md block truncate'
@@ -225,20 +234,20 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
className='h-8 text-xs font-medium w-full'
onClick={() => setShowClearConfirm(true)}
>
立即清空缓存
{t('clearCacheNow')}
</Button>
</div>
{/* 右边:配置区 (3/5 cols) */}
<div className='lg:col-span-3 border-t lg:border-t-0 lg:border-l border-border/40 pt-6 lg:pt-0 lg:pl-6 space-y-4'>
<h4 className='text-xs font-semibold text-muted-foreground uppercase tracking-wider'>
策略配置
{t('policyConfig')}
</h4>
<form onSubmit={handleSaveConfig} className='space-y-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
<div className='space-y-1.5'>
<Label htmlFor='maxSizeMB' className='text-xs font-medium'>
最大容量限制 (MB)
{t('maxSizeLimit')}
</Label>
<Input
id='maxSizeMB'
@@ -251,13 +260,13 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
required
/>
<p className='text-[9px] text-muted-foreground'>
当总大小超出该值时,自动触发淘汰算法
{t('maxSizeHint')}
</p>
</div>
<div className='space-y-1.5'>
<Label htmlFor='ttlMinutes' className='text-xs font-medium'>
生存时间限制 (分钟)
{t('ttlLimit')}
</Label>
<Input
id='ttlMinutes'
@@ -270,7 +279,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
required
/>
<p className='text-[9px] text-muted-foreground'>
缓存项的最长存活时间,超期后将失效被清理
{t('ttlHint')}
</p>
</div>
</div>
@@ -281,10 +290,10 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
htmlFor='lruEnabled'
className='text-xs font-semibold block cursor-pointer'
>
启用 LRU 淘汰机制
{t('enableLru')}
</Label>
<span className='text-[10px] text-muted-foreground block'>
在到达最大容量限制时,自动移除最久未被访问的缓存项。关闭该功能仅清理过期项。
{t('enableLruDesc')}
</span>
</div>
<Switch
@@ -300,7 +309,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
disabled={savingConfig}
className='h-8 text-xs px-4'
>
{savingConfig ? '正在保存...' : '保存设置'}
{savingConfig ? t('saving') : t('saveSettings')}
</Button>
</div>
</form>
@@ -309,7 +318,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
) : (
<div className='flex flex-col items-center justify-center py-10 text-muted-foreground'>
<HardDrive className='size-8 opacity-45 mb-2' />
<span className='text-xs'>未加载到缓存状态信息</span>
<span className='text-xs'>{t('noCacheStatus')}</span>
</div>
)}
</CardContent>
@@ -320,11 +329,10 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
<DialogHeader>
<DialogTitle className='text-sm font-semibold flex items-center gap-2 text-destructive'>
<Trash2 className='size-4' />
确认清空所有缓存?
{t('confirmClearAllCache')}
</DialogTitle>
<DialogDescription className='text-xs text-muted-foreground pt-1'>
该操作将彻底清空磁盘目录下的全部缓存文件(包含临时解压、处理后的图片及各种块文件),重置键数量统计为
0。此操作不可撤销,且可能导致用户拉取资源时出现一过性的响应变慢。
{t('confirmClearDesc')}
</DialogDescription>
</DialogHeader>
<DialogFooter className='flex gap-2 justify-end mt-4'>
@@ -335,7 +343,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
onClick={() => setShowClearConfirm(false)}
disabled={clearingCache}
>
取消
{t('cancel')}
</Button>
<Button
variant='destructive'
@@ -344,7 +352,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
onClick={handleClearCache}
disabled={clearingCache}
>
{clearingCache ? '清理中...' : '确认清空'}
{clearingCache ? t('clearing') : t('confirmClear')}
</Button>
</DialogFooter>
</DialogContent>
@@ -7,6 +7,7 @@ import CodeMirror from '@uiw/react-codemirror';
import { sql } from '@codemirror/lang-sql';
import { toast } from 'sonner';
import { ArrowLeft, Play, RefreshCw, Terminal, Trash2 } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui/button';
import {
@@ -49,6 +50,7 @@ interface SQLConsoleProps {
}
export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
const t = useTranslations('admin.database.sql');
// SQL 控制台状态
const [sqlQuery, setSqlQuery] = useState<string>('');
const [executingSQL, setExecutingSQL] = useState<boolean>(false);
@@ -97,10 +99,10 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
try {
const result = await DbManageService.executeSQL(sqlQuery);
setSqlResult(result);
toast.success('SQL 执行成功');
toast.success(t('sqlExecSuccess'));
} catch (err) {
setSqlError(err instanceof Error ? err.message : '未知执行错误');
toast.error('SQL 执行失败');
toast.error(t('sqlExecFailed'));
} finally {
setExecutingSQL(false);
}
@@ -134,7 +136,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
<Terminal className='size-5 text-primary' />
<div>
<h1 className='text-2xl font-semibold tracking-tight'>
SQL 查询终端
{t('sqlQueryTerminal')}
</h1>
</div>
</div>
@@ -153,34 +155,34 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
<div className='flex items-center justify-between px-4 py-2 border-b bg-muted/40 shrink-0 gap-4 flex-wrap'>
<div className='flex items-center gap-2'>
<Terminal className='size-4 text-primary' />
<span className='text-xs font-semibold'>SQL 编辑器</span>
<span className='text-xs font-semibold'>{t('sqlEditor')}</span>
</div>
<div className='flex items-center gap-3 flex-wrap'>
{/* 快速模板 */}
<div className='flex items-center gap-1.5'>
<span className='text-[11px] text-muted-foreground'>
快速模板:
{t('quickTemplates')}
</span>
<Select onValueChange={handlePresetSQLChange}>
<SelectTrigger className='h-7 w-[180px] text-[11px] bg-background'>
<SelectValue placeholder='选择预设 SQL' />
<SelectValue placeholder={t('selectPresetSQL')} />
</SelectTrigger>
<SelectContent>
<SelectItem value='SELECT * FROM users LIMIT 10;'>
查询用户 (SELECT users)
{t('queryUsers')}
</SelectItem>
<SelectItem value='SELECT * FROM uploads LIMIT 10;'>
查询文件 (SELECT uploads)
{t('queryFiles')}
</SelectItem>
<SelectItem value='SELECT * FROM task_executions ORDER BY created_at DESC LIMIT 10;'>
查询任务流水 (SELECT task_executions)
{t('queryTaskExecutions')}
</SelectItem>
<SelectItem value='SELECT sqlite_version();'>
SQLite 版本 (SQLite only)
{t('sqliteVersion')}
</SelectItem>
<SelectItem value='SELECT version();'>
PostgreSQL 版本 (Postgres only)
{t('postgresVersion')}
</SelectItem>
</SelectContent>
</Select>
@@ -204,7 +206,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
disabled={executingSQL || !sqlQuery.trim()}
>
<Play className='size-3' />
{executingSQL ? '运行中...' : '运行'}
{executingSQL ? t('running') : t('run')}
</Button>
</div>
</div>
@@ -245,10 +247,10 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
<div className='flex-1 min-h-[100px] flex flex-col overflow-hidden bg-muted/10'>
{/* 结果栏工具提示 */}
<div className='flex items-center justify-between px-4 py-1.5 border-b bg-muted/20 shrink-0 text-[11px] text-muted-foreground font-mono'>
<span className='font-semibold'>执行输出</span>
<span className='font-semibold'>{t('executionOutput')}</span>
{sqlResult && (
<span>
类型: {sqlResult.type.toUpperCase()} | 耗时:{' '}
{t('type')}: {sqlResult.type.toUpperCase()} | {t('duration')}:{' '}
{sqlResult.execution_time_ms} ms
</span>
)}
@@ -260,7 +262,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
<div className='flex flex-col items-center justify-center h-full py-10 space-y-2'>
<RefreshCw className='size-5 text-primary animate-spin' />
<span className='text-xs text-muted-foreground'>
正在在数据库执行查询,请稍候...
{t('executingQuery')}
</span>
</div>
)}
@@ -319,7 +321,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
colSpan={sqlResult.columns.length}
className='text-center py-10 text-muted-foreground'
>
查询结果为空
{t('queryResultEmpty')}
</TableCell>
</TableRow>
)}
@@ -331,11 +333,13 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
{sqlResult.type === 'exec' && (
<div className='bg-primary/5 border border-primary/10 text-primary-foreground font-mono text-xs p-6 rounded-lg text-center my-auto'>
<p className='text-foreground text-sm font-semibold'>
SQL 执行成功
{t('sqlExecSuccessTitle')}
</p>
<p className='text-muted-foreground mt-2'>
受影响行数: {sqlResult.affected_rows} 行,耗时{' '}
{sqlResult.execution_time_ms} 毫秒。
{t('affectedRows', {
rows: sqlResult.affected_rows,
ms: sqlResult.execution_time_ms,
})}
</p>
</div>
)}
@@ -345,9 +349,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
{!executingSQL && !sqlResult && !sqlError && (
<div className='flex flex-col items-center justify-center h-full py-10 text-muted-foreground opacity-60'>
<Terminal className='size-8 mb-2' />
<span className='text-xs'>
编辑器就绪,请在上方编写 SQL 并运行查看结果
</span>
<span className='text-xs'>{t('editorReady')}</span>
</div>
)}
</div>
@@ -4,6 +4,7 @@ import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'sonner';
import { Layers, RefreshCw } from 'lucide-react';
import { useTranslations } from 'next-intl';
import {
Card,
@@ -67,6 +68,7 @@ export function TableBrowser({
loadingTables,
refreshTrigger,
}: TableBrowserProps) {
const t = useTranslations('admin.database.tableBrowser');
const [selectedTable, setSelectedTable] = useState<string>('');
const [tableData, setTableData] = useState<TableDataResponse | null>(null);
const [page, setPage] = useState<number>(1);
@@ -125,9 +127,11 @@ export function TableBrowser({
<Card className='border-border/40 bg-card/50 backdrop-blur-sm shadow-sm'>
<CardHeader className='pb-3 border-b border-dashed flex flex-col md:flex-row md:items-center md:justify-between gap-4'>
<div className='space-y-0.5'>
<CardTitle className='text-sm font-semibold'>数据表浏览器</CardTitle>
<CardTitle className='text-sm font-semibold'>
{t('tableBrowser')}
</CardTitle>
<CardDescription className='text-[11px]'>
浏览数据库中的物理数据表详情及内容
{t('tableBrowserDesc')}
</CardDescription>
</div>
@@ -137,7 +141,7 @@ export function TableBrowser({
) : (
<Select value={selectedTable} onValueChange={handleTableChange}>
<SelectTrigger className='h-8 w-[200px] text-xs bg-background border-border/40'>
<SelectValue placeholder='选择数据表' />
<SelectValue placeholder={t('selectTable')} />
</SelectTrigger>
<SelectContent className='max-h-[300px]'>
{tables.map((t) => (
@@ -202,7 +206,7 @@ export function TableBrowser({
colSpan={tableData.columns.length}
className='text-center py-10 text-muted-foreground'
>
表中无记录数据
{t('noRecordsInTable')}
</TableCell>
</TableRow>
)}
@@ -213,11 +217,11 @@ export function TableBrowser({
{/* 分页控制 */}
<div className='flex items-center justify-between text-xs text-muted-foreground flex-wrap gap-2 pt-2 border-t border-dashed'>
<div>
总行数:{' '}
{t('totalRows')}:{' '}
<span className='font-mono text-foreground font-semibold'>
{formatNumber(tableData.total)}
</span>{' '}
条记录
{t('records')}
</div>
<div className='flex items-center gap-2'>
<Button
@@ -227,11 +231,13 @@ export function TableBrowser({
onClick={() => handlePageChange(page - 1)}
disabled={page <= 1 || loadingData}
>
上一页
{t('prevPage')}
</Button>
<span className='text-xs px-2 font-mono'>
第 {page} /{' '}
{Math.max(1, Math.ceil(tableData.total / pageSize))} 页
{t('pageIndicator', {
current: page,
total: Math.max(1, Math.ceil(tableData.total / pageSize)),
})}
</span>
<Button
variant='outline'
@@ -242,7 +248,7 @@ export function TableBrowser({
page >= Math.ceil(tableData.total / pageSize) || loadingData
}
>
下一页
{t('nextPage')}
</Button>
</div>
</div>
@@ -250,7 +256,7 @@ export function TableBrowser({
) : (
<div className='flex flex-col items-center justify-center py-10 text-muted-foreground'>
<Layers className='size-8 opacity-45 mb-2' />
<span className='text-xs'>未选择数据表或表结构无法加载</span>
<span className='text-xs'>{t('noTableSelected')}</span>
</div>
)}
</CardContent>
@@ -16,6 +16,7 @@ import {
Server,
Terminal,
} from 'lucide-react';
import { useTranslations } from 'next-intl';
import {
Card,
@@ -57,6 +58,7 @@ const formatNumber = (num: number | string) => {
};
export function DatabasePageClient() {
const t = useTranslations('admin.database');
// 核心状态
const [overview, setOverview] = useState<DBOverview | null>(null);
const [tables, setTables] = useState<string[]>([]);
@@ -69,19 +71,22 @@ export function DatabasePageClient() {
const [refreshTrigger, setRefreshTrigger] = useState<number>(0);
// 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);
}
}, []);
const fetchOverview = useCallback(
async (isSilent = false) => {
if (!isSilent) setLoadingOverview(true);
try {
const data = await services.dbManage.getOverview();
setOverview(data);
} catch (err) {
toast.error(t('fetchOverviewFailed'), {
description: err instanceof Error ? err.message : '未知错误',
});
} finally {
setLoadingOverview(false);
}
},
[t],
);
// 2. 获取表列表
const fetchTables = useCallback(async () => {
@@ -90,13 +95,13 @@ export function DatabasePageClient() {
const data = await services.dbManage.listTables();
setTables(data);
} catch (err) {
toast.error('获取数据库数据表列表失败', {
toast.error(t('fetchTablesFailed'), {
description: err instanceof Error ? err.message : '未知错误',
});
} finally {
setLoadingTables(false);
}
}, []);
}, [t]);
// 3. 协调刷新
const handleRefreshAll = () => {
@@ -118,11 +123,11 @@ export function DatabasePageClient() {
a.click();
a.remove();
URL.revokeObjectURL(url);
toast.success('数据库导出成功', {
description: `已下载归档文件: ${filename}`,
toast.success(t('exportSuccess'), {
description: t('exportSuccessDesc', { filename }),
});
} catch (err) {
toast.error('数据库导出失败', {
toast.error(t('exportFailed'), {
description: err instanceof Error ? err.message : '导出异常',
});
} finally {
@@ -170,7 +175,9 @@ export function DatabasePageClient() {
<div className='flex items-center gap-2'>
<Database className='size-5 text-primary' />
<div>
<h1 className='text-2xl font-semibold tracking-tight'>数据管理</h1>
<h1 className='text-2xl font-semibold tracking-tight'>
{t('pageTitle')}
</h1>
</div>
</div>
<Button
@@ -183,7 +190,7 @@ export function DatabasePageClient() {
<RefreshCw
className={`size-3 ${loadingOverview ? 'animate-spin' : ''}`}
/>
刷新数据
{t('refreshData')}
</Button>
</div>
@@ -196,7 +203,7 @@ export function DatabasePageClient() {
<Card className='shadow-sm border-border/40 bg-card/50 backdrop-blur-sm hover:border-primary/20 transition-all duration-300'>
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
<CardDescription className='text-[10px] font-medium'>
数据库类型
{t('dbType')}
</CardDescription>
<Server className='size-3.5 text-muted-foreground' />
</CardHeader>
@@ -211,7 +218,7 @@ export function DatabasePageClient() {
<Card className='shadow-sm border-border/40 bg-card/50 backdrop-blur-sm hover:border-primary/20 transition-all duration-300 col-span-1 md:col-span-2 lg:col-span-1'>
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
<CardDescription className='text-[10px] font-medium'>
版本信息
{t('versionInfo')}
</CardDescription>
<Cpu className='size-3.5 text-muted-foreground' />
</CardHeader>
@@ -229,7 +236,7 @@ export function DatabasePageClient() {
<Card className='shadow-sm border-border/40 bg-card/50 backdrop-blur-sm hover:border-primary/20 transition-all duration-300'>
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
<CardDescription className='text-[10px] font-medium'>
名称/路径
{t('nameOrPath')}
</CardDescription>
<Database className='size-3.5 text-muted-foreground' />
</CardHeader>
@@ -249,7 +256,7 @@ export function DatabasePageClient() {
<Card className='shadow-sm border-border/40 bg-card/50 backdrop-blur-sm hover:border-primary/20 transition-all duration-300'>
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
<CardDescription className='text-[10px] font-medium'>
数据库大小
{t('dbSize')}
</CardDescription>
<HardDrive className='size-3.5 text-muted-foreground' />
</CardHeader>
@@ -262,7 +269,7 @@ export function DatabasePageClient() {
<Card className='shadow-sm border-border/40 bg-card/50 backdrop-blur-sm hover:border-primary/20 transition-all duration-300'>
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
<CardDescription className='text-[10px] font-medium'>
物理数据表
{t('physicalTables')}
</CardDescription>
<Layers className='size-3.5 text-muted-foreground' />
</CardHeader>
@@ -277,7 +284,7 @@ export function DatabasePageClient() {
<Card className='shadow-sm border-border/40 bg-card/50 backdrop-blur-sm hover:border-primary/20 transition-all duration-300'>
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
<CardDescription className='text-[10px] font-medium'>
活跃连接数
{t('activeConnections')}
</CardDescription>
<Activity className='size-3.5 text-muted-foreground' />
</CardHeader>
@@ -303,9 +310,11 @@ export function DatabasePageClient() {
{/* 4. 底部功能卡片区 */}
<Card className='border-border/40 bg-card/50 backdrop-blur-sm shadow-sm'>
<CardHeader className='pb-3 border-b border-dashed'>
<CardTitle className='text-sm font-semibold'>功能区</CardTitle>
<CardTitle className='text-sm font-semibold'>
{t('featureArea')}
</CardTitle>
<CardDescription className='text-[11px]'>
数据库导出及自定义高级 SQL 执行终端
{t('featureAreaDesc')}
</CardDescription>
</CardHeader>
<CardContent className='pt-4'>
@@ -315,11 +324,10 @@ export function DatabasePageClient() {
<div className='space-y-1 pr-4'>
<p className='text-xs font-semibold flex items-center gap-1.5'>
<Download className='size-4 text-primary' />
数据库备份导出
{t('dbBackupExport')}
</p>
<p className='text-[10px] text-muted-foreground'>
直接导出并下载物理数据库镜像文件(SQLite 导出 .db,PostgreSQL
导出为打包的 .sql 文本文件)
{t('dbBackupExportDesc')}
</p>
</div>
<Button
@@ -332,7 +340,7 @@ export function DatabasePageClient() {
<RefreshCw
className={`size-3 ${exporting ? 'animate-spin' : ''}`}
/>
{exporting ? '正在准备...' : '开始导出'}
{exporting ? t('preparing') : t('startExport')}
</Button>
</div>
@@ -341,11 +349,10 @@ export function DatabasePageClient() {
<div className='space-y-1 pr-4'>
<p className='text-xs font-semibold flex items-center gap-1.5'>
<Terminal className='size-4 text-primary' />
SQL 查询控台
{t('sqlQueryConsole')}
</p>
<p className='text-[10px] text-muted-foreground'>
打开在线 SQL 控制终端,执行原生的 SQL
进行数据筛选、更新、调试或性能优化
{t('sqlQueryConsoleDesc')}
</p>
</div>
<Button
@@ -356,7 +363,7 @@ export function DatabasePageClient() {
}}
>
<FileText className='size-3.5' />
进入控台
{t('enterConsole')}
</Button>
</div>
</div>