feat: 管理界面优化 — 移动端自适应 + 数据源 + LLM 配置

移动端自适应:
- AdminLayout: 汉堡菜单 + 可折叠侧边栏 + 遮罩层 + ESC 关闭
- 所有页面响应式布局 (grid-cols-1 sm:grid-cols-2 lg:grid-cols-4)
- 触摸友好按钮 (min-h-[44px])
- 表格移动端卡片视图

新增页面:
- DataSources: 数据源状态 + API Key 脱敏 + 测试连接
- LLMConfig: LLM 配置 + 测试连接 + 使用统计 + 最近预测

增强功能:
- Config: 配置列表 + 修改指南 + 快捷导航
- types: 新增 DataSourceStatus, LLMUsageStats 等类型
- dal: 新增 testDataSource, fetchDataSourceStatuses, testLLMConnection 等
This commit is contained in:
shangfangjian
2026-09-17 02:43:44 +08:00
parent 6680da7d61
commit 91e406f5ee
14 changed files with 1312 additions and 174 deletions
+173 -44
View File
@@ -1,60 +1,189 @@
/**
* Admin 后台 - 配置管理
* Admin 后台 - 系统配置管理页面
*
* 提示: API Key 配置通过 .env 文件管理,不在前端明文存储。
* 功能:
* - 显示当前 .env 配置(脱敏)
* - 提供配置修改指南
* - 快速导航到数据源和 LLM 配置
*/
import { useEffect, useState, useCallback } from 'react'
import { fetchSystemConfig } from '../dal'
import { Card, CardBody, CardHeader, Badge, SectionHeader } from '../components'
export default function ConfigPage() {
const [config, setConfig] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const loadConfig = useCallback(async () => {
setLoading(true)
try {
const data = await fetchSystemConfig()
setConfig(data)
} catch {
setConfig([])
} finally {
setLoading(false)
}
}, [])
useEffect(() => { loadConfig() }, [loadConfig])
return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold text-white"></h2>
<p className="mt-1 text-sm text-gray-400">API Key </p>
</div>
<SectionHeader
title="系统配置"
description="查看当前系统配置参数(通过 .env 文件管理)"
/>
<div className="rounded border border-yellow-500/30 bg-yellow-500/10 p-4">
<p className="text-sm text-yellow-300">
API Key <code className="rounded bg-gray-800 px-1">.env</code> ,
</p>
</div>
<div className="rounded border border-gray-700 bg-gray-800 p-4">
<h3 className="mb-3 text-sm font-medium text-white"></h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between border-b border-gray-700 py-2">
<span className="text-gray-400">LLM_API_KEY</span>
<span className="text-gray-300"> .env </span>
</div>
<div className="flex justify-between border-b border-gray-700 py-2">
<span className="text-gray-400">LLM_BASE_URL</span>
<span className="text-gray-300"> .env </span>
</div>
<div className="flex justify-between border-b border-gray-700 py-2">
<span className="text-gray-400">BZZOIRO_KEY</span>
<span className="text-gray-300"> .env </span>
</div>
<div className="flex justify-between py-2">
<span className="text-gray-400">API_FOOTBALL_KEY</span>
<span className="text-gray-300"> .env </span>
{/* 配置警告 */}
<div className="rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-4">
<div className="flex items-start gap-3">
<span className="text-yellow-400 text-lg"></span>
<div>
<p className="text-sm font-medium text-yellow-300"></p>
<p className="mt-1 text-xs text-yellow-300/80">
(API Key) <code className="rounded bg-gray-800/50 px-1">.env</code>
, SSH
</p>
</div>
</div>
</div>
<div className="rounded border border-gray-700 bg-gray-800 p-4">
<h3 className="mb-3 text-sm font-medium text-white"></h3>
<p className="text-sm text-gray-400">
SSH NAS,:
</p>
<pre className="mt-2 rounded bg-gray-900 p-3 text-xs text-green-400">
{`cd /vol2/1000/Docker/Profeto
# 编辑 .env 文件
LLM_API_KEY=sk-你的真实密钥
BZZOIRO_KEY=你的密钥
# 重启后端
docker compose restart api`}
</pre>
{/* 快速导航 */}
<div className="grid gap-4 sm:grid-cols-2">
<a
href="/admin/data-sources"
className="flex items-center gap-3 rounded-lg border border-gray-800 bg-gray-900 p-4 transition-colors hover:border-gray-700 hover:bg-gray-800/50"
>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-500/10 text-blue-400 text-lg">
</div>
<div>
<div className="text-sm font-medium text-gray-200"></div>
<div className="text-xs text-gray-500"> API Key</div>
</div>
</a>
<a
href="/admin/llm-config"
className="flex items-center gap-3 rounded-lg border border-gray-800 bg-gray-900 p-4 transition-colors hover:border-gray-700 hover:bg-gray-800/50"
>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-400 text-lg">
</div>
<div>
<div className="text-sm font-medium text-gray-200">LLM </div>
<div className="text-xs text-gray-500"></div>
</div>
</a>
</div>
{/* 配置列表 */}
<Card>
<CardHeader
title="当前配置"
action={
<button
onClick={loadConfig}
className="rounded px-2 py-1 text-xs text-blue-400 hover:bg-gray-800 min-h-[44px] min-w-[44px]"
>
</button>
}
/>
<CardBody>
{loading ? (
<div className="space-y-3">
{[1, 2, 3, 4, 5].map(i => (
<div key={i} className="h-10 animate-pulse rounded bg-gray-800" />
))}
</div>
) : config.length > 0 ? (
<div className="space-y-1">
{/* 桌面端表头 */}
<div className="hidden sm:grid sm:grid-cols-3 gap-4 border-b border-gray-800 px-3 py-2 text-xs text-gray-500">
<span></span>
<span></span>
<span></span>
</div>
{/* 配置行 */}
{config.map(item => (
<div
key={item.key}
className="flex flex-col sm:grid sm:grid-cols-3 gap-2 sm:gap-4 border-b border-gray-800/50 px-3 py-3 hover:bg-gray-800/30 rounded-lg sm:rounded-none"
>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-300">{item.key}</span>
{item.is_sensitive && (
<Badge status="warning"></Badge>
)}
</div>
<div className="font-mono text-xs text-gray-400 break-all">
{item.value_masked}
</div>
<div className="text-xs text-gray-500">
{item.description}
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-sm text-gray-500"></div>
)}
</CardBody>
</Card>
{/* 修改指南 */}
<Card>
<CardHeader title="修改配置指南" />
<CardBody className="space-y-4">
<div className="rounded-lg border border-gray-800 p-4">
<h4 className="text-sm font-medium text-gray-200 mb-2"> SSH .env</h4>
<pre className="overflow-x-auto rounded bg-gray-950 p-3 text-xs text-green-400 leading-relaxed">
{`# 连接到 NAS
ssh user@your-nas-ip
# 进入项目目录
cd /vol2/1000/Docker/Profeto
# 编辑 .env 文件
nano .env
# 修改后重启后端服务
docker compose restart api
# 查看日志确认生效
docker compose logs -f api`}
</pre>
</div>
<div className="rounded-lg border border-gray-800 p-4">
<h4 className="text-sm font-medium text-gray-200 mb-2"></h4>
<div className="space-y-2 text-xs">
<div className="flex items-start gap-2">
<code className="flex-shrink-0 rounded bg-gray-800 px-1.5 py-0.5 text-blue-400">LLM_API_KEY</code>
<span className="text-gray-400">LLM API ,</span>
</div>
<div className="flex items-start gap-2">
<code className="flex-shrink-0 rounded bg-gray-800 px-1.5 py-0.5 text-blue-400">LLM_MODEL</code>
<span className="text-gray-400">使, gpt-4oclaude-3-5-sonnet</span>
</div>
<div className="flex items-start gap-2">
<code className="flex-shrink-0 rounded bg-gray-800 px-1.5 py-0.5 text-blue-400">LLM_BASE_URL</code>
<span className="text-gray-400">API , OpenAI </span>
</div>
<div className="flex items-start gap-2">
<code className="flex-shrink-0 rounded bg-gray-800 px-1.5 py-0.5 text-blue-400">BZZOIRO_KEY</code>
<span className="text-gray-400">Bzzoiro API </span>
</div>
<div className="flex items-start gap-2">
<code className="flex-shrink-0 rounded bg-gray-800 px-1.5 py-0.5 text-blue-400">DATABASE_URL</code>
<span className="text-gray-400">PostgreSQL </span>
</div>
</div>
</div>
</CardBody>
</Card>
</div>
)
}