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
+80 -12
View File
@@ -1,11 +1,13 @@
/**
* Admin 后台 - 布局组件
*
* 提供管理界面的基本结构:顶栏 + 侧边栏 + 内容区
* 响应式布局: 移动端汉堡菜单 + 可折叠侧边栏
* 桌面端: 固定侧边栏 + 内容区
* 暗色主题,参考线性风格设计
*/
import { NavLink, Outlet } from 'react-router-dom'
import { useState, useEffect, useCallback } from 'react'
import { NavLink, Outlet, useLocation } from 'react-router-dom'
const NAV_ITEMS = [
{ to: '/admin', label: '仪表盘', icon: '◇', end: true },
@@ -13,22 +15,72 @@ const NAV_ITEMS = [
{ to: '/admin/predictions', label: '预测管理', icon: '◆' },
{ to: '/admin/backtest', label: '回测管理', icon: '◉' },
{ to: '/admin/monitoring', label: '监控面板', icon: '◐' },
{ to: '/admin/config', label: '配置管理', icon: '' },
{ to: '/admin/data-sources', label: '数据源', icon: '' },
{ to: '/admin/llm-config', label: 'LLM 配置', icon: '◬' },
{ to: '/admin/config', label: '系统配置', icon: '◑' },
]
export default function AdminLayout() {
const [sidebarOpen, setSidebarOpen] = useState(false)
const location = useLocation()
// 路由变化时关闭移动端菜单
const closeSidebar = useCallback(() => setSidebarOpen(false), [])
useEffect(() => {
closeSidebar()
}, [location.pathname, closeSidebar])
// ESC 键关闭菜单
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') setSidebarOpen(false)
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [])
return (
<div className="flex h-screen bg-gray-950 text-gray-200 overflow-hidden">
{/* ── 移动端遮罩层 ── */}
{sidebarOpen && (
<div
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm lg:hidden"
onClick={() => setSidebarOpen(false)}
aria-hidden="true"
/>
)}
{/* ── 侧边栏 ── */}
<aside className="flex w-56 flex-shrink-0 flex-col border-r border-gray-800 bg-gray-900">
<div className="border-b border-gray-800 px-5 py-4">
<aside
className={`
fixed inset-y-0 left-0 z-50 flex w-64 flex-shrink-0 flex-col border-r border-gray-800 bg-gray-900
transform transition-transform duration-200 ease-in-out
lg:relative lg:z-auto lg:translate-x-0
${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
`}
aria-label="主导航"
>
{/* Logo */}
<div className="flex items-center justify-between border-b border-gray-800 px-5 py-4">
<h1 className="font-serif text-lg font-bold tracking-wider text-gray-100">
Profeto
<span className="ml-2 text-xs font-normal tracking-normal text-gray-500">Admin</span>
</h1>
{/* 移动端关闭按钮 */}
<button
onClick={() => setSidebarOpen(false)}
className="rounded-md p-2 text-gray-400 hover:bg-gray-800 hover:text-gray-200 lg:hidden min-h-[44px] min-w-[44px]"
aria-label="关闭菜单"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<nav className="flex-1 overflow-y-auto px-3 py-4" aria-label="主导航">
{/* 导航 */}
<nav className="flex-1 overflow-y-auto px-3 py-4">
<ul className="space-y-0.5">
{NAV_ITEMS.map(item => (
<li key={item.to}>
@@ -36,7 +88,7 @@ export default function AdminLayout() {
to={item.to}
end={item.end}
className={({ isActive }) =>
`flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors ${
`flex items-center gap-3 rounded-md px-3 py-2.5 text-sm transition-colors min-h-[44px] ${
isActive
? 'bg-gray-800 text-white font-medium'
: 'text-gray-400 hover:bg-gray-800/60 hover:text-gray-200'
@@ -53,10 +105,11 @@ export default function AdminLayout() {
</ul>
</nav>
{/* 底部 */}
<div className="border-t border-gray-800 px-4 py-3">
<a
href="/"
className="flex items-center gap-2 text-xs text-gray-500 transition-colors hover:text-gray-300"
className="flex items-center gap-2 text-xs text-gray-500 transition-colors hover:text-gray-300 min-h-[44px]"
>
<span aria-hidden="true"></span>
@@ -67,18 +120,33 @@ export default function AdminLayout() {
{/* ── 主内容区 ── */}
<div className="flex flex-1 flex-col overflow-hidden">
{/* 顶栏 */}
<header className="flex h-12 flex-shrink-0 items-center justify-between border-b border-gray-800 bg-gray-900 px-6">
<header className="flex h-12 flex-shrink-0 items-center justify-between border-b border-gray-800 bg-gray-900 px-4 lg:px-6">
{/* 移动端菜单按钮 */}
<button
onClick={() => setSidebarOpen(true)}
className="rounded-md p-2 text-gray-400 hover:bg-gray-800 hover:text-gray-200 lg:hidden min-h-[44px] min-w-[44px]"
aria-label="打开菜单"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
{/* 状态指示 */}
<div className="flex items-center gap-2 text-xs text-gray-500">
<span className="inline-block h-2 w-2 rounded-full bg-emerald-500" aria-hidden="true" />
<span className="hidden sm:inline"></span>
</div>
{/* 版本 */}
<div className="text-xs text-gray-500">
Profeto Admin v1.0
<span className="hidden sm:inline">Profeto Admin v1.0</span>
<span className="sm:hidden">v1.0</span>
</div>
</header>
{/* 页面内容 */}
<main className="flex-1 overflow-y-auto p-6">
<main className="flex-1 overflow-y-auto p-4 lg:p-6">
<Outlet />
</main>
</div>