6081fb7e48
Co-authored-by: Cursor <cursoragent@cursor.com>
141 lines
3.9 KiB
TypeScript
141 lines
3.9 KiB
TypeScript
import { type ReactNode, useEffect } from "react";
|
|
import { NavLink, Navigate, Route, Routes } from "react-router-dom";
|
|
import {
|
|
clearSession,
|
|
getToken,
|
|
getUsername,
|
|
startAuthTokenAutoRefresh,
|
|
} from "./api/client";
|
|
import FundsBar from "./components/FundsBar";
|
|
import LoginPage from "./pages/Login";
|
|
import PlanPage from "./pages/Plan";
|
|
import TradesPage from "./pages/Trades";
|
|
import StatsPage from "./pages/Stats";
|
|
import SettingsPage from "./pages/Settings";
|
|
|
|
const NAV = [
|
|
{ to: "/plan", label: "监控", full: "自动对冲计划" },
|
|
{ to: "/trades", label: "记录", full: "交易记录" },
|
|
{ to: "/stats", label: "统计", full: "统计" },
|
|
{ to: "/settings", label: "设置", full: "系统设置" },
|
|
] as const;
|
|
|
|
function Shell({ children }: { children: ReactNode }) {
|
|
const user = getUsername();
|
|
return (
|
|
<div className="app-shell">
|
|
<div className="app-container">
|
|
<header className="app-header-block">
|
|
<div className="brand-row">
|
|
<div className="brand">
|
|
<img
|
|
className="brand-mark"
|
|
src="/icons/brand.png"
|
|
alt=""
|
|
width={28}
|
|
height={28}
|
|
/>
|
|
<span className="brand-full">比特骆驼自动化对冲系统</span>
|
|
<span className="brand-short">比特骆驼</span>
|
|
</div>
|
|
<div className="brand-actions">
|
|
<span className="meta mono topnav-user">{user}</span>
|
|
<button
|
|
className="btn ghost topnav-logout"
|
|
type="button"
|
|
onClick={() => {
|
|
clearSession();
|
|
window.location.href = "/login";
|
|
}}
|
|
>
|
|
退出
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<nav className="top-nav" aria-label="主导航">
|
|
{NAV.map((item) => (
|
|
<NavLink
|
|
key={item.to}
|
|
to={item.to}
|
|
className={({ isActive }) => (isActive ? "active" : "")}
|
|
>
|
|
<span className="nav-label-full">{item.full}</span>
|
|
<span className="nav-label-short">{item.label}</span>
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
</header>
|
|
|
|
<FundsBar />
|
|
|
|
<main className="page">{children}</main>
|
|
</div>
|
|
|
|
<nav className="bottom-nav" aria-label="手机导航">
|
|
{NAV.map((item) => (
|
|
<NavLink
|
|
key={item.to}
|
|
to={item.to}
|
|
className={({ isActive }) =>
|
|
isActive ? "bottom-nav-item active" : "bottom-nav-item"
|
|
}
|
|
>
|
|
<span className="bottom-nav-label">{item.label}</span>
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RequireAuth({ children }: { children: ReactNode }) {
|
|
useEffect(() => {
|
|
if (!getToken()) return;
|
|
return startAuthTokenAutoRefresh();
|
|
}, []);
|
|
if (!getToken()) return <Navigate to="/login" replace />;
|
|
return <Shell>{children}</Shell>;
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<Routes>
|
|
<Route path="/login" element={<LoginPage />} />
|
|
<Route
|
|
path="/plan"
|
|
element={
|
|
<RequireAuth>
|
|
<PlanPage />
|
|
</RequireAuth>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/trades"
|
|
element={
|
|
<RequireAuth>
|
|
<TradesPage />
|
|
</RequireAuth>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/stats"
|
|
element={
|
|
<RequireAuth>
|
|
<StatsPage />
|
|
</RequireAuth>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/settings"
|
|
element={
|
|
<RequireAuth>
|
|
<SettingsPage />
|
|
</RequireAuth>
|
|
}
|
|
/>
|
|
<Route path="/" element={<Navigate to="/plan" replace />} />
|
|
<Route path="*" element={<Navigate to="/plan" replace />} />
|
|
</Routes>
|
|
);
|
|
}
|