feat: 基础框架搭建完毕
Checks / verify (push) Failing after 2m37s

This commit is contained in:
2026-10-07 09:27:47 +08:00
parent ba6dfffab7
commit bffdb3858b
64 changed files with 326 additions and 332 deletions
+62
View File
@@ -0,0 +1,62 @@
import { useEffect, useRef, useState } from "react"
import { Activity, ArrowUpRight, BarChart3, Bell, BookOpen, ChevronRight, CircleDot, Database, FlaskConical, LayoutDashboard, LockKeyhole, RefreshCw, Server, Settings2, ShieldCheck, Sparkles, Users } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { getHealth, type Health } from "@/lib/api"
import {type User} from "@/lib/auth"
import {UsersPanel} from "@/components/UsersPanel"
const workspaces = [
{name:"总览", icon:LayoutDashboard}, {name:"候选审核",icon:BookOpen}, {name:"策略中心",icon:Settings2},
{name:"回测研究",icon:FlaskConical}, {name:"数据健康",icon:Database}, {name:"模型配置",icon:Sparkles},
{name:"通知任务",icon:Bell}, {name:"用户管理",icon:Users}, {name:"审计日志",icon:ShieldCheck}, {name:"系统设置",icon:LockKeyhole},
]
const label = {ready:"连接正常",unavailable:"无法连接",not_configured:"待配置"}
export default function App({user,onLogout,onSessionExpired}:{user:User;onLogout:()=>Promise<void>;onSessionExpired:()=>void}) {
const [page,setPage] = useState("总览")
const [health,setHealth] = useState<Health | null>(null)
const [error,setError] = useState("")
const [loading,setLoading] = useState(false)
const [logoutError,setLogoutError] = useState("")
const [signingOut,setSigningOut] = useState(false)
async function signOut() {setSigningOut(true);setLogoutError("");try {await onLogout()} catch(e) {setLogoutError(e instanceof Error ? e.message : "退出失败,请重试。")} finally {setSigningOut(false)}}
const controller = useRef<AbortController | null>(null)
async function refresh() {
controller.current?.abort()
const next = new AbortController()
controller.current = next
setLoading(true)
try { const data = await getHealth(next.signal); if (!next.signal.aborted) {setHealth(data);setError("")} }
catch (e) { if (!next.signal.aborted) {setHealth(null);setError(e instanceof Error ? e.message : "暂时无法连接 API 服务")} }
finally {if (!next.signal.aborted) setLoading(false)}
}
useEffect(()=>{void refresh();return ()=>controller.current?.abort()},[])
const overview = page === "总览" || page === "数据健康"
return <div className="min-h-screen lg:grid lg:grid-cols-[238px_1fr]">
<aside className="flex flex-col border-r bg-card px-5 py-7 lg:min-h-screen">
<a href="#" onClick={()=>setPage("总览")} className="flex items-center gap-3 px-2 text-foreground no-underline"><span className="flex size-10 items-center justify-center rounded-xl bg-foreground font-serif text-xl text-card">V</span><div><strong className="text-base">见势 · Veyra</strong><p className="mt-1 text-[10px] tracking-[.2em] text-muted-foreground">OPERATIONS CONSOLE</p></div></a>
<p className="mb-3 mt-10 px-3 text-[10px] tracking-[.18em] text-muted-foreground">工作空间</p>
<nav aria-label="管理端工作区" className="grid grid-cols-2 gap-1 sm:grid-cols-5 lg:grid-cols-1">{workspaces.map(({name,icon:Icon})=><button key={name} onClick={()=>setPage(name)} aria-current={page===name ? "page" : undefined} className={`flex items-center gap-3 rounded-lg px-3 py-3 text-left text-sm transition-colors ${page===name ? "bg-accent font-medium text-accent-foreground" : "text-muted-foreground hover:bg-muted"}`}><Icon size={17}/>{name}{page===name && <ChevronRight size={14} className="ml-auto"/>}</button>)}</nav>
<div className="mt-auto hidden pt-12 lg:block"><div className="rounded-lg border p-3 text-xs text-muted-foreground"><div className="mb-2 flex items-center gap-2 text-foreground"><CircleDot size={14}/>开发环境</div>第一阶段 · 工程与服务基础</div></div>
</aside>
<div className="min-w-0">
<header className="flex h-20 items-center justify-between border-b px-6 lg:px-10"><div className="flex items-center gap-2 text-sm text-muted-foreground">工作空间 <ChevronRight size={13}/><span className="text-foreground">{page}</span></div><div className="flex items-center gap-3"><span className="hidden text-xs text-muted-foreground sm:block">{user.display_name}</span><Badge variant="outline">LOCAL · 开发环境</Badge><Button variant="ghost" size="sm" onClick={()=>void signOut()} disabled={signingOut}>{signingOut ? "正在退出" : "退出"}</Button></div></header>
<main className="mx-auto max-w-7xl p-6 lg:p-10">
{logoutError && <p role="alert" className="mb-6 text-sm text-destructive">{logoutError}</p>}
<div className="mb-8 flex flex-wrap items-end justify-between gap-4"><div><p className="mb-3 text-[10px] tracking-[.2em] text-primary">VEYRA / {overview ? "SYSTEM OVERVIEW" : "WORKSPACE"}</p><h1 className="text-3xl font-semibold tracking-tight">{page === "总览" ? "把每一步,建立在依据之上。" : page}</h1><p className="mt-3 text-sm text-muted-foreground">{overview ? "查看服务连接与建设进度,从可靠的基础开始。" : "该工作区的业务功能正在建设中。"}</p></div>{overview && <Button variant="outline" onClick={()=>void refresh()} disabled={loading}><RefreshCw className={loading ? "animate-spin" : ""}/>{loading ? "检查中" : "刷新状态"}</Button>}</div>
{overview ? <>
{error && <div role="alert" className="mb-6 rounded-lg border border-destructive/25 bg-destructive/5 p-4 text-sm text-destructive">无法读取当前服务状态。请检查 API 是否启动。{error}</div>}
<div className="mb-7 grid gap-4 md:grid-cols-3">{[
{title:"API 服务",subtitle:"Go · Gin",icon:Server,value:loading ? "检查中" : health ? "运行中" : "未连接"},
{title:"PostgreSQL",subtitle:"业务事实与版本化迁移",icon:Database,value:loading ? "检查中" : health ? label[health.components.postgresql.status] : "未知"},
{title:"Redis",subtitle:"缓存与订阅索引",icon:Activity,value:loading ? "检查中" : health ? label[health.components.redis.status] : "未知"},
].map(({title,subtitle,icon:Icon,value})=><Card key={title}><CardHeader><div className="flex items-center justify-between"><CardDescription>{title}</CardDescription><Icon size={18} className="text-muted-foreground"/></div></CardHeader><CardContent><p className="mb-2 text-2xl font-semibold">{value}</p><p className="text-xs text-muted-foreground">{subtitle}</p></CardContent></Card>)}</div>
<div className="grid gap-6 xl:grid-cols-[1.5fr_1fr]"><Card><CardHeader><div className="flex items-center justify-between"><CardTitle>第一条产品闭环</CardTitle><Badge>建设中</Badge></div><CardDescription className="mt-2">先验证完整流程,再扩大策略与数据范围。</CardDescription></CardHeader><CardContent><ol className="space-y-5">{["管理端审核并发布观察候选","用户查看证据,确认买入计划","条件满足后发送提醒","记录实际成交,建立持仓监控","记录退出成交,生成成本后复盘"].map((v,i)=><li key={v} className="flex items-center gap-4"><span className="flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-xs text-muted-foreground">0{i+1}</span><span className="text-sm">{v}</span><span className="ml-auto text-xs text-muted-foreground">待实现</span></li>)}</ol></CardContent></Card><Card><CardHeader><BarChart3 size={24} className="mb-4 text-primary"/><CardTitle>数据接入后,开始观察。</CardTitle><CardDescription className="mt-2 leading-7">候选数量、策略表现与账户指标将在业务功能与数据接入完成后展示。</CardDescription></CardHeader><CardContent><div className="rounded-lg bg-muted p-4 text-xs leading-6 text-muted-foreground">当前状态来源:Go API 实时探测<br/>最近检查:{health ? new Date(health.checked_at).toLocaleString("zh-CN") : "尚未取得状态"}</div><Button variant="ghost" className="mt-4 w-full justify-between" onClick={()=>setPage("数据健康")}>查看数据健康<ArrowUpRight/></Button></CardContent></Card></div>
</> : page==="用户管理" ? <UsersPanel onSessionExpired={onSessionExpired}/> : <Card><CardContent className="flex min-h-80 flex-col items-center justify-center p-10 text-center"><div className="mb-5 rounded-2xl bg-muted p-5"><LockKeyhole size={28} className="text-muted-foreground"/></div><h2 className="text-lg font-semibold">{page}尚未开放</h2><p className="mt-3 max-w-md text-sm leading-7 text-muted-foreground">该模块完成业务接口、权限与验证后开放。</p><Button variant="outline" className="mt-6" onClick={()=>setPage("总览")}>返回总览</Button></CardContent></Card>}
<footer className="mt-10 flex items-center justify-between border-t pt-5 text-[11px] text-muted-foreground"><span>见势 · Veyra / 可追溯的投资决策</span><span>工程基础 v0.1</span></footer>
</main>
</div>
</div>
}