UI: centered nav title, settings tabs, running status, manual buttons.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-24 17:17:44 +08:00
parent 8ea05e7064
commit b6d9877538
7 changed files with 236 additions and 145 deletions
+10 -5
View File
@@ -40,6 +40,11 @@ class StrategyEngine:
rest_left = 0 rest_left = 0
if rest_until: if rest_until:
rest_left = max(0, int((int(rest_until) - time.time() * 1000) / 1000)) rest_left = max(0, int((int(rest_until) - time.time() * 1000) / 1000))
last_error = row["last_error"]
# 清掉已修复的旧序列化错误残留
if last_error and "PriceResult" in str(last_error) and "__dict__" in str(last_error):
self._set_state(last_error=None)
last_error = None
return { return {
"running": bool(row["running"]), "running": bool(row["running"]),
"phase": row["phase"], "phase": row["phase"],
@@ -51,7 +56,7 @@ class StrategyEngine:
"rest_seconds": rest_sec, "rest_seconds": rest_sec,
"exit_move_points": exit_pts, "exit_move_points": exit_pts,
"can_open": can_open_new(open_hhmm=s.open_hhmm, stop_hhmm=s.stop_open_hhmm), "can_open": can_open_new(open_hhmm=s.open_hhmm, stop_hhmm=s.stop_open_hhmm),
"last_error": row["last_error"], "last_error": last_error,
"position": upl, "position": upl,
"ledger": self.ledger.snapshot(), "ledger": self.ledger.snapshot(),
} }
@@ -67,16 +72,16 @@ class StrategyEngine:
sql = f"UPDATE strategy_state SET {', '.join(cols)} WHERE id=1" sql = f"UPDATE strategy_state SET {', '.join(cols)} WHERE id=1"
self.db.execute(sql, tuple(vals)) self.db.execute(sql, tuple(vals))
async def pause(self) -> dict[str, Any]:
self._set_state(running=0, phase="paused", last_error=None)
return self.state()
async def start(self) -> dict[str, Any]: async def start(self) -> dict[str, Any]:
self._set_state(running=1, last_error=None, phase="idle") self._set_state(running=1, last_error=None, phase="idle")
if self._task is None or self._task.done(): if self._task is None or self._task.done():
self._task = asyncio.create_task(self._loop(), name="strategy-engine") self._task = asyncio.create_task(self._loop(), name="strategy-engine")
return self.state() return self.state()
async def pause(self) -> dict[str, Any]:
self._set_state(running=0, phase="paused")
return self.state()
async def emergency_close(self) -> dict[str, Any]: async def emergency_close(self) -> dict[str, Any]:
async with self._lock: async with self._lock:
r = self.matcher.close_group(reason="emergency") r = self.matcher.close_group(reason="emergency")
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>eth_hedge_sim</title> <title>OKX 自动化对冲系统</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link
+32 -27
View File
@@ -11,33 +11,38 @@ function Shell({ children }: { children: ReactNode }) {
const user = getUsername(); const user = getUsername();
return ( return (
<div className="app-shell"> <div className="app-shell">
<nav className="topnav"> <header className="topnav">
<div className="brand">eth_hedge_sim</div> <div className="topnav-side left">
<NavLink to="/plan" className={({ isActive }) => (isActive ? "active" : "")}> <div className="brand">OKX </div>
</div>
</NavLink> <nav className="topnav-center">
<NavLink to="/trades" className={({ isActive }) => (isActive ? "active" : "")}> <NavLink to="/plan" className={({ isActive }) => (isActive ? "active" : "")}>
</NavLink> </NavLink>
<NavLink to="/stats" className={({ isActive }) => (isActive ? "active" : "")}> <NavLink to="/trades" className={({ isActive }) => (isActive ? "active" : "")}>
</NavLink> </NavLink>
<NavLink to="/settings" className={({ isActive }) => (isActive ? "active" : "")}> <NavLink to="/stats" className={({ isActive }) => (isActive ? "active" : "")}>
</NavLink> </NavLink>
<div className="spacer" /> <NavLink to="/settings" className={({ isActive }) => (isActive ? "active" : "")}>
<span className="meta mono">{user}</span>
<button </NavLink>
className="btn ghost" </nav>
type="button" <div className="topnav-side right">
onClick={() => { <span className="meta mono">{user}</span>
clearSession(); <button
window.location.href = "/login"; className="btn ghost"
}} type="button"
> onClick={() => {
退 clearSession();
</button> window.location.href = "/login";
</nav> }}
>
退
</button>
</div>
</header>
<main className="page">{children}</main> <main className="page">{children}</main>
</div> </div>
); );
+1 -1
View File
@@ -27,7 +27,7 @@ export default function LoginPage() {
return ( return (
<div className="login-wrap"> <div className="login-wrap">
<form className="login-box" onSubmit={onSubmit}> <form className="login-box" onSubmit={onSubmit}>
<h1>eth_hedge_sim</h1> <h1>OKX </h1>
<p></p> <p></p>
{err ? <div className="err">{err}</div> : null} {err ? <div className="err">{err}</div> : null}
<div className="field"> <div className="field">
+8 -4
View File
@@ -90,7 +90,7 @@ export default function PlanPage() {
disabled={!!busy} disabled={!!busy}
onClick={() => act("/api/sim/open-group", "open")} onClick={() => act("/api/sim/open-group", "open")}
> >
</button> </button>
<button <button
className="btn ghost" className="btn ghost"
@@ -98,7 +98,7 @@ export default function PlanPage() {
disabled={!!busy} disabled={!!busy}
onClick={() => act("/api/sim/close-group", "close")} onClick={() => act("/api/sim/close-group", "close")}
> >
</button> </button>
<button <button
className="btn ghost" className="btn ghost"
@@ -115,8 +115,12 @@ export default function PlanPage() {
<div className="kv"> <div className="kv">
<span></span> <span></span>
<span className="mono"> <span className="mono">
{plan?.running ? "运行中" : "已停"} · {plan?.phase || "—"} · {" "} {plan?.running ? (
{plan?.rounds_done ?? 0}/{plan?.max_rounds ?? 3} <span className="status-running"></span>
) : (
<span className="status-stopped"></span>
)}{" "}
· {plan?.phase || "—"} · {plan?.rounds_done ?? 0}/{plan?.max_rounds ?? 3}
</span> </span>
</div> </div>
<div className="kv"> <div className="kv">
+128 -105
View File
@@ -7,7 +7,11 @@ import {
StrategySettings, StrategySettings,
} from "../api/client"; } from "../api/client";
type Tab = "strategy" | "account";
export default function SettingsPage() { export default function SettingsPage() {
const [tab, setTab] = useState<Tab>("strategy");
const [newUsername, setNewUsername] = useState(getUsername() || "admin"); const [newUsername, setNewUsername] = useState(getUsername() || "admin");
const [currentPassword, setCurrentPassword] = useState(""); const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
@@ -85,113 +89,132 @@ export default function SettingsPage() {
} }
return ( return (
<div style={{ display: "grid", gap: 16, maxWidth: 560 }}> <div style={{ maxWidth: 560 }}>
<div className="card"> <h2 style={{ marginTop: 0 }}></h2>
<h2 style={{ marginTop: 0 }}></h2> <div className="tabs">
<p style={{ color: "var(--muted)" }}> <button
N =1× type="button"
</p> className={tab === "strategy" ? "tab active" : "tab"}
{stratOk ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{stratOk}</div> : null} onClick={() => setTab("strategy")}
<form onSubmit={onSaveStrategy}> >
<div className="field">
<label htmlFor="exit">EXIT_MOVE_POINTS</label> </button>
<input <button
id="exit" type="button"
className="mono" className={tab === "account" ? "tab active" : "tab"}
type="number" onClick={() => setTab("account")}
step="1" >
value={exitPts}
onChange={(e) => setExitPts(Number(e.target.value))} </button>
/>
</div>
<div className="field">
<label htmlFor="rest">REST_SECONDS</label>
<input
id="rest"
className="mono"
type="number"
step="1"
value={rest}
onChange={(e) => setRest(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="rounds">MAX_ROUNDS</label>
<input
id="rounds"
className="mono"
type="number"
step="1"
value={maxRounds}
onChange={(e) => setMaxRounds(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="fee">FEE_RATE</label>
<input
id="fee"
className="mono"
type="number"
step="0.0001"
value={fee}
onChange={(e) => setFee(Number(e.target.value))}
/>
</div>
<button className="btn" type="submit">
</button>
</form>
</div> </div>
<div className="card"> {tab === "strategy" ? (
<h2 style={{ marginTop: 0 }}></h2> <div className="card">
{err ? <div className="err">{err}</div> : null} <p style={{ color: "var(--muted)", marginTop: 0 }}>
{ok ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{ok}</div> : null} N =1×
<form onSubmit={onSaveCreds}> </p>
<div className="field"> {stratOk ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{stratOk}</div> : null}
<label htmlFor="user"></label> {err && tab === "strategy" ? <div className="err">{err}</div> : null}
<input <form onSubmit={onSaveStrategy}>
id="user" <div className="field">
value={newUsername} <label htmlFor="exit">EXIT_MOVE_POINTS</label>
onChange={(e) => setNewUsername(e.target.value)} <input
required id="exit"
/> className="mono"
</div> type="number"
<div className="field"> step="1"
<label htmlFor="cur"></label> value={exitPts}
<input onChange={(e) => setExitPts(Number(e.target.value))}
id="cur" />
type="password" </div>
value={currentPassword} <div className="field">
onChange={(e) => setCurrentPassword(e.target.value)} <label htmlFor="rest">REST_SECONDS</label>
required <input
/> id="rest"
</div> className="mono"
<div className="field"> type="number"
<label htmlFor="np"></label> step="1"
<input value={rest}
id="np" onChange={(e) => setRest(Number(e.target.value))}
type="password" />
value={newPassword} </div>
onChange={(e) => setNewPassword(e.target.value)} <div className="field">
required <label htmlFor="rounds">MAX_ROUNDS</label>
/> <input
</div> id="rounds"
<div className="field"> className="mono"
<label htmlFor="cp"></label> type="number"
<input step="1"
id="cp" value={maxRounds}
type="password" onChange={(e) => setMaxRounds(Number(e.target.value))}
value={confirmPassword} />
onChange={(e) => setConfirmPassword(e.target.value)} </div>
required <div className="field">
/> <label htmlFor="fee">FEE_RATE</label>
</div> <input
<button className="btn" type="submit" disabled={loading}> id="fee"
{loading ? "保存中…" : "保存账号"} className="mono"
</button> type="number"
</form> step="0.0001"
</div> value={fee}
onChange={(e) => setFee(Number(e.target.value))}
/>
</div>
<button className="btn" type="submit">
</button>
</form>
</div>
) : (
<div className="card">
{err ? <div className="err">{err}</div> : null}
{ok ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{ok}</div> : null}
<form onSubmit={onSaveCreds}>
<div className="field">
<label htmlFor="user"></label>
<input
id="user"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
required
/>
</div>
<div className="field">
<label htmlFor="cur"></label>
<input
id="cur"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
/>
</div>
<div className="field">
<label htmlFor="np"></label>
<input
id="np"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
/>
</div>
<div className="field">
<label htmlFor="cp"></label>
<input
id="cp"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
</div>
<button className="btn" type="submit" disabled={loading}>
{loading ? "保存中…" : "保存账号"}
</button>
</form>
</div>
)}
</div> </div>
); );
} }
+56 -2
View File
@@ -49,7 +49,8 @@ input {
} }
.topnav { .topnav {
display: flex; display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 10px 16px; padding: 10px 16px;
@@ -61,11 +62,31 @@ input {
z-index: 10; z-index: 10;
} }
.topnav-side {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.topnav-side.right {
justify-content: flex-end;
}
.topnav-center {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.brand { .brand {
font-weight: 700; font-weight: 700;
letter-spacing: 0.02em; letter-spacing: 0.02em;
margin-right: 12px;
color: var(--accent); color: var(--accent);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.topnav a { .topnav a {
@@ -73,6 +94,7 @@ input {
text-decoration: none; text-decoration: none;
padding: 8px 12px; padding: 8px 12px;
border-radius: 6px; border-radius: 6px;
white-space: nowrap;
} }
.topnav a.active { .topnav a.active {
@@ -80,6 +102,38 @@ input {
background: var(--bg-panel); background: var(--bg-panel);
} }
.tabs {
display: flex;
gap: 4px;
margin-bottom: 12px;
border-bottom: 1px solid var(--line);
padding-bottom: 0;
}
.tab {
background: transparent;
border: 0;
color: var(--muted);
padding: 10px 14px;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.tab.active {
color: var(--text);
border-bottom-color: var(--accent);
}
.status-running {
color: var(--up);
font-weight: 600;
}
.status-stopped {
color: var(--muted);
}
.topnav .spacer { .topnav .spacer {
flex: 1; flex: 1;
} }