feat: 管理界面报刊风统一改造 — 移除独立暗色主题,预测/回测/监控/配置页增强
This commit is contained in:
@@ -1,20 +1,26 @@
|
||||
/**
|
||||
* Admin 后台 - 系统配置管理页面
|
||||
* Admin 后台 - 系统配置管理页面(报刊风)
|
||||
*
|
||||
* 功能:
|
||||
* - 显示当前 .env 配置(脱敏)
|
||||
* - 提供配置修改指南
|
||||
* - 快速导航到数据源和 LLM 配置
|
||||
* - 管理员密钥(X-API-Key):存本机浏览器,自动附带到采集/回测/结算等受保护接口
|
||||
* - 显示当前 .env 配置(脱敏;后端暂无配置端点,为静态说明)
|
||||
* - 配置修改指南
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { fetchSystemConfig } from '../dal'
|
||||
import { Card, CardBody, CardHeader, Badge, SectionHeader } from '../components'
|
||||
import { getAdminKey, setAdminKey } from '../api'
|
||||
import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner, SkeletonBlock } from '../components'
|
||||
|
||||
export default function ConfigPage() {
|
||||
const [config, setConfig] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// 管理员密钥
|
||||
const [adminKey, setAdminKeyInput] = useState('')
|
||||
const [keySaved, setKeySaved] = useState(false)
|
||||
const [keyExists, setKeyExists] = useState(false)
|
||||
|
||||
const loadConfig = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -27,54 +33,102 @@ export default function ConfigPage() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => { loadConfig() }, [loadConfig])
|
||||
useEffect(() => {
|
||||
loadConfig()
|
||||
const stored = getAdminKey()
|
||||
setKeyExists(stored !== '')
|
||||
}, [loadConfig])
|
||||
|
||||
function handleSaveKey(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setAdminKey(adminKey.trim())
|
||||
setKeyExists(adminKey.trim() !== '')
|
||||
setKeySaved(true)
|
||||
setAdminKeyInput('')
|
||||
setTimeout(() => setKeySaved(false), 3000)
|
||||
}
|
||||
|
||||
function handleClearKey() {
|
||||
setAdminKey('')
|
||||
setAdminKeyInput('')
|
||||
setKeyExists(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SectionHeader
|
||||
title="系统配置"
|
||||
description="查看当前系统配置参数(通过 .env 文件管理)"
|
||||
description="管理员密钥管理与系统参数查看。敏感配置一律通过服务器 .env 文件管理。"
|
||||
/>
|
||||
|
||||
{/* 配置警告 */}
|
||||
<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 连接到服务器编辑配置文件。
|
||||
{/* 管理员密钥 */}
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="管理员密钥 (X-API-Key)"
|
||||
description="后端设置 ADMIN_API_KEY 后,采集 / 回测 / 结算等接口需要此密钥"
|
||||
/>
|
||||
<CardBody>
|
||||
<form onSubmit={handleSaveKey} className="space-y-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<input
|
||||
type="password"
|
||||
value={adminKey}
|
||||
onChange={e => setAdminKeyInput(e.target.value)}
|
||||
placeholder={keyExists ? '••••••••(已保存,输入新值可更换)' : '粘贴 ADMIN_API_KEY'}
|
||||
autoComplete="off"
|
||||
className="field flex-1"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={!adminKey.trim()} className="btn btn-solid">
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearKey}
|
||||
disabled={!keyExists}
|
||||
className="btn btn-sm"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{keySaved && <Alert kind="ok" title="密钥已保存,后续请求将自动附带" />}
|
||||
{keyExists && !keySaved && (
|
||||
<p className="text-2xs text-ink-500">
|
||||
当前状态:<Badge status="success">已保存密钥</Badge>
|
||||
<span className="ml-2">密钥仅保存在本机浏览器,不会上传到任何第三方。</span>
|
||||
</p>
|
||||
)}
|
||||
<p className="border-l-2 border-ink-300 pl-3 text-2xs leading-relaxed text-ink-500">
|
||||
密钥与服务器 .env 中 ADMIN_API_KEY 一致即可。留空时后端默认不鉴权(本地开发模式)。
|
||||
遇到 401 错误通常就是缺这个密钥。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 快速导航 */}
|
||||
<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"
|
||||
className="group flex items-center justify-between border border-ink-300 p-4 transition-colors hover:border-ink-900 hover:bg-paper-100"
|
||||
>
|
||||
<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 className="font-serif text-sm font-bold text-ink-900">数据源配置</div>
|
||||
<div className="mt-0.5 text-2xs text-ink-500">管理采集源 API Key</div>
|
||||
</div>
|
||||
<span className="text-ink-300 transition-colors group-hover:text-press" aria-hidden="true">→</span>
|
||||
</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"
|
||||
className="group flex items-center justify-between border border-ink-300 p-4 transition-colors hover:border-ink-900 hover:bg-paper-100"
|
||||
>
|
||||
<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 className="font-serif text-sm font-bold text-ink-900">LLM 配置</div>
|
||||
<div className="mt-0.5 text-2xs text-ink-500">管理模型连接与统计</div>
|
||||
</div>
|
||||
<span className="text-ink-300 transition-colors group-hover:text-press" aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -82,53 +136,38 @@ export default function ConfigPage() {
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="当前配置"
|
||||
description="脱敏展示,实际值在服务器 .env 文件中"
|
||||
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 onClick={loadConfig} disabled={loading} className="btn btn-sm">
|
||||
{loading ? (<><Spinner /> 加载中</>) : '刷新'}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<CardBody>
|
||||
<CardBody className="px-0 sm:px-0">
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3 px-4 sm:px-5">
|
||||
{[1, 2, 3, 4, 5].map(i => (
|
||||
<div key={i} className="h-10 animate-pulse rounded bg-gray-800" />
|
||||
<SkeletonBlock key={i} className="h-9 w-full" />
|
||||
))}
|
||||
</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>
|
||||
{/* 配置行 */}
|
||||
<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"
|
||||
className="flex flex-col gap-1 border-b border-ink-200 px-4 py-2.5 last:border-b-0 sm:grid sm:grid-cols-[minmax(0,2fr)_minmax(0,3fr)_minmax(0,2fr)] sm:items-baseline sm:gap-4 sm:px-5"
|
||||
>
|
||||
<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}
|
||||
<span className="font-mono text-xs text-ink-800">{item.key}</span>
|
||||
{item.is_sensitive && <Badge status="warning">敏感</Badge>}
|
||||
</div>
|
||||
<div className="break-all font-mono text-2xs text-ink-500">{item.value_masked}</div>
|
||||
<div className="text-2xs text-ink-400">{item.description}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-sm text-gray-500">无法加载配置信息</div>
|
||||
<p className="py-8 text-center text-xs text-ink-400">无法加载配置信息</p>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
@@ -136,12 +175,12 @@ export default function ConfigPage() {
|
||||
{/* 修改指南 */}
|
||||
<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
|
||||
<CardBody className="space-y-5">
|
||||
<div>
|
||||
<h4 className="mb-2 font-serif text-sm font-bold text-ink-900">通过 SSH 修改 .env</h4>
|
||||
<pre className="overflow-x-auto border border-ink-200 bg-paper-100 p-3 font-mono text-2xs leading-relaxed text-ink-700">
|
||||
{`# 连接到部署主机
|
||||
ssh user@your-server-ip
|
||||
|
||||
# 进入项目目录
|
||||
cd /vol2/1000/Docker/Profeto
|
||||
@@ -157,29 +196,24 @@ 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-4o、claude-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>
|
||||
<h4 className="mb-2 font-serif text-sm font-bold text-ink-900">常用配置项说明</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
['LLM_API_KEY', 'LLM 服务商的 API 密钥,用于调用大模型'],
|
||||
['LLM_MODEL', '使用的模型名称,如 gpt-4o、claude-3-5-sonnet'],
|
||||
['LLM_BASE_URL', 'API 基础地址,支持兼容 OpenAI 协议的服务商'],
|
||||
['ADMIN_API_KEY', '管理后台写接口的鉴权密钥,配置后需在本页保存到浏览器'],
|
||||
['BZZOIRO_KEY', 'Bzzoiro 数据源 API 密钥'],
|
||||
['DATABASE_URL', 'PostgreSQL 数据库连接字符串'],
|
||||
].map(([key, desc]) => (
|
||||
<div key={key} className="flex items-start gap-2.5">
|
||||
<code className="flex-shrink-0 border border-ink-200 bg-paper-100 px-1.5 py-0.5 font-mono text-2xs text-ink-800">
|
||||
{key}
|
||||
</code>
|
||||
<span className="text-xs leading-relaxed text-ink-600">{desc}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
|
||||
Reference in New Issue
Block a user