fix: 修复 Admin 后台多个 bug

- api.ts: 修复 URL 拼接 bug(缺少 API_BASE) 和 204 判断逻辑
- dal.ts: 修复 fetchDashboard 类型错乱,修正 API 端点
- types.ts: 对齐后端 Pydantic 模型
- 所有页面: Badge variant→status, CardHeader children→action
- 移除不存在的后端端点调用
This commit is contained in:
shangfangjian
2026-09-17 02:11:50 +08:00
parent d3284c48c3
commit 6680da7d61
9 changed files with 572 additions and 1642 deletions
+47 -232
View File
@@ -1,245 +1,60 @@
/**
* Admin 后台 - 配置管理页面
* Admin 后台 - 配置管理
*
* 功能:
* - 查看当前 API Key 配置(脱敏显示)
* - 更新 LLM / 数据源密钥
* - 配置分类管理(LLM / 数据源 / 系统)
* 提示: API Key 配置通过 .env 文件管理,不在前端明文存储。
*/
import { useEffect, useState } from 'react'
import { fetchConfigs, updateConfig } from '../dal'
import type { ConfigEntry } from '../types'
import { Card, CardBody, CardHeader, Badge, EmptyState } from '../components'
import { SectionHeader } from '../components'
const CATEGORY_LABELS: Record<string, string> = {
llm: 'LLM 服务',
datasource: '数据源',
system: '系统配置',
}
const CATEGORY_ORDER = ['llm', 'datasource', 'system']
export default function ConfigPage() {
const [configs, setConfigs] = useState<ConfigEntry[]>([])
const [loading, setLoading] = useState(true)
const [editing, setEditing] = useState<string | null>(null)
const [editValue, setEditValue] = useState('')
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const [successMsg, setSuccessMsg] = useState<string | null>(null)
useEffect(() => {
fetchConfigs()
.then(setConfigs)
.catch(err => setError(err instanceof Error ? err.message : '加载配置失败'))
.finally(() => setLoading(false))
}, [])
function startEdit(key: string, currentValue: string) {
setEditing(key)
setEditValue(currentValue.replace(/\*+$/, '')) // 去掉脱敏星号
setError(null)
setSuccessMsg(null)
}
async function saveEdit(key: string) {
setSaving(true)
setError(null)
setSuccessMsg(null)
try {
await updateConfig({ key, value: editValue })
setSuccessMsg(`${key} 已更新`)
setEditing(null)
// 刷新列表
const updated = await fetchConfigs()
setConfigs(updated)
} catch (err) {
setError(err instanceof Error ? err.message : '保存失败')
} finally {
setSaving(false)
}
}
// 按分类分组
const grouped = CATEGORY_ORDER.reduce(
(acc, cat) => {
acc[cat] = configs.filter(c => c.category === cat)
return acc
},
{} as Record<string, ConfigEntry[]>,
)
return (
<div className="space-y-6">
<SectionHeader
title="配置管理"
description="管理 API Key 和系统参数(存储在 .env 文件)"
/>
<div>
<h2 className="text-xl font-semibold text-white"></h2>
<p className="mt-1 text-sm text-gray-400">API Key </p>
</div>
{error && (
<div className="rounded-md border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-400">
{error}
</div>
)}
{successMsg && (
<div className="rounded-md border border-emerald-500/30 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-400">
{successMsg}
</div>
)}
<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>
{loading ? (
<Card>
<CardBody>
<EmptyState text="加载配置中..." />
</CardBody>
</Card>
) : configs.length === 0 ? (
<Card>
<CardBody>
<div className="py-8 text-center">
<p className="text-sm text-gray-500"></p>
<p className="mt-1 text-xs text-gray-600">
API, .env
</p>
</div>
</CardBody>
</Card>
) : (
CATEGORY_ORDER.map(cat => {
const items = grouped[cat]
if (!items || items.length === 0) return null
return (
<Card key={cat}>
<CardHeader title={CATEGORY_LABELS[cat]} />
<CardBody className="p-0">
<div className="divide-y divide-gray-800">
{items.map(cfg => (
<ConfigRow
key={cfg.key}
config={cfg}
editing={editing === cfg.key}
editValue={editValue}
saving={saving}
onEditValueChange={setEditValue}
onStartEdit={() => startEdit(cfg.key, cfg.value)}
onSave={() => saveEdit(cfg.key)}
onCancel={() => setEditing(null)}
/>
))}
</div>
</CardBody>
</Card>
)
})
)}
{/* 配置说明 */}
<Card>
<CardHeader title="配置说明" />
<CardBody>
<div className="space-y-3 text-xs text-gray-500">
<p>
<strong className="text-gray-400">LLM :</strong> API Key
( OpenAIAnthropic)
</p>
<p>
<strong className="text-gray-400">:</strong>
( UnderstatBzzoiro)
</p>
<p>
<strong className="text-gray-400">:</strong>
</p>
<p className="rounded-md border border-yellow-500/20 bg-yellow-500/5 px-3 py-2 text-yellow-400/80">
</p>
<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>
</CardBody>
</Card>
</div>
)
}
function ConfigRow({
config,
editing,
editValue,
saving,
onEditValueChange,
onStartEdit,
onSave,
onCancel,
}: {
config: ConfigEntry
editing: boolean
editValue: string
saving: boolean
onEditValueChange: (v: string) => void
onStartEdit: () => void
onSave: () => void
onCancel: () => void
}) {
return (
<div className="flex items-center gap-4 px-5 py-3">
{/* 键名 */}
<div className="w-48 flex-shrink-0">
<div className="font-mono text-xs text-gray-400">{config.key}</div>
{config.description && (
<div className="mt-0.5 text-[10px] text-gray-600">{config.description}</div>
)}
</div>
{/* 值 */}
<div className="flex-1">
{editing ? (
<div className="flex items-center gap-2">
<input
type="text"
value={editValue}
onChange={e => onEditValueChange(e.target.value)}
className="flex-1 rounded-md border border-gray-600 bg-gray-800 px-2 py-1 text-xs text-gray-200 focus:border-blue-500 focus:outline-none"
autoFocus
onKeyDown={e => {
if (e.key === 'Enter') onSave()
if (e.key === 'Escape') onCancel()
}}
/>
<button
onClick={onSave}
disabled={saving}
className="rounded bg-blue-600 px-2 py-1 text-xs text-white hover:bg-blue-700 disabled:opacity-50"
>
</button>
<button
onClick={onCancel}
className="rounded border border-gray-700 px-2 py-1 text-xs text-gray-400 hover:border-gray-600"
>
</button>
</div>
) : (
<div className="flex items-center gap-2">
<code className="font-mono text-xs text-gray-300">{config.value}</code>
{config.updated_at && (
<span className="text-[10px] text-gray-600">
{new Date(config.updated_at).toLocaleDateString('zh-CN')}
</span>
)}
</div>
)}
</div>
{/* 操作 */}
{!editing && (
<button
onClick={onStartEdit}
className="flex-shrink-0 rounded border border-gray-700 px-2 py-1 text-xs text-gray-400 hover:border-gray-600"
>
</button>
)}
<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>
</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>
</div>
)
}