arbitrage-engine/frontend/app/dashboard/page.tsx
dev-worker 18506f2a44 fix: P1/P2/P3剩余6项全部修复
P1-3: 前端持仓USDT从config读riskUsd(不再硬编码*2)
P1-4: 平仓兜底不盲目取最后成交,无明确平仓记录则延后结算
P2-1: LISTEN连接断线自动重建+重新LISTEN
P2-2: 余额风控LOW_BALANCE自动恢复(余额回升则解除暂停)
P2-3: fetch_pending_signals改用asyncio.to_thread避免阻塞事件循环
P3-1: dashboard页面改用新auth体系(authFetch+useAuth+/api/auth/me)
2026-03-02 16:19:03 +00:00

124 lines
5.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { authFetch, useAuth } from "@/lib/auth";
interface UserInfo {
id: number;
email: string;
discord_id: string | null;
tier: string;
expires_at: string | null;
}
export default function DashboardPage() {
const router = useRouter();
const { isLoggedIn, loading, logout } = useAuth();
const [user, setUser] = useState<UserInfo | null>(null);
const [discordId, setDiscordId] = useState("");
const [saving, setSaving] = useState(false);
const [msg, setMsg] = useState("");
useEffect(() => {
if (loading) return;
if (!isLoggedIn) { router.push("/login"); return; }
authFetch("/api/auth/me")
.then(r => r.ok ? r.json() : Promise.reject())
.then(d => {
setUser({
id: d.id,
email: d.email,
discord_id: d.discord_id || null,
tier: d.subscription?.tier || "free",
expires_at: d.subscription?.expires_at || null,
});
setDiscordId(d.discord_id || "");
})
.catch(() => router.push("/login"));
}, [loading, isLoggedIn, router]);
const bindDiscord = async () => {
setSaving(true); setMsg("");
try {
const r = await authFetch("/api/user/bind-discord", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ discord_id: discordId }),
});
const d = await r.json();
setMsg(r.ok ? "\u2705 绑定成功" : d.detail || "绑定失败");
if (r.ok && user) setUser({ ...user, discord_id: discordId });
} catch { setMsg("绑定失败"); }
setSaving(false);
};
if (loading || !user) return <div className="text-slate-500 p-8">...</div>;
const tierLabel: Record<string, string> = { free: "免费版", pro: "Pro", premium: "Premium" };
return (
<div className="space-y-6 max-w-2xl">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-slate-900"></h1>
<button onClick={logout} className="text-sm text-slate-500 hover:text-slate-800">退</button>
</div>
<div className="rounded-xl border border-slate-200 bg-white p-6 space-y-3">
<h2 className="text-slate-800 font-semibold"></h2>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="text-slate-500"></div>
<div className="text-slate-800">{user.email}</div>
<div className="text-slate-500"></div>
<div className="text-blue-600 font-medium">{tierLabel[user.tier] || user.tier}</div>
<div className="text-slate-500"></div>
<div className="text-slate-800">{user.expires_at ? new Date(user.expires_at).toLocaleDateString("zh-CN") : "永久免费"}</div>
</div>
</div>
<div className="rounded-xl border border-slate-200 bg-white p-6 space-y-4">
<h2 className="text-slate-800 font-semibold">Discord </h2>
<p className="text-slate-500 text-sm">Discord ID后@你</p>
<div className="flex gap-2">
<input
value={discordId} onChange={e => setDiscordId(e.target.value)}
className="flex-1 bg-white border border-slate-200 rounded-lg px-3 py-2 text-slate-900 text-sm focus:outline-none focus:border-cyan-500"
placeholder="Discord用户ID18位数字"
/>
<button
onClick={bindDiscord} disabled={saving || !discordId}
className="bg-cyan-600 hover:bg-cyan-500 disabled:opacity-50 text-white px-4 py-2 rounded-lg text-sm"
>
{saving ? "保存中..." : "绑定"}
</button>
</div>
{msg && <p className={`text-sm ${msg.startsWith("\u2705") ? "text-emerald-400" : "text-red-400"}`}>{msg}</p>}
<p className="text-slate-400 text-xs">Discord ID ID</p>
</div>
<div className="rounded-xl border border-slate-200 bg-white p-6">
<h2 className="text-slate-800 font-semibold mb-3"></h2>
<div className="grid grid-cols-3 gap-3 text-sm">
{[
{ tier: "free", label: "免费版", price: "¥0", features: ["实时费率面板"] },
{ tier: "pro", label: "Pro", price: "¥99/月", features: ["实时费率面板", "信号Discord推送", "历史数据"] },
{ tier: "premium", label: "Premium", price: "¥299/月", features: ["Pro全部功能", "定制阈值", "优先客服"] },
].map(p => (
<div key={p.tier} className={`rounded-lg border p-4 space-y-2 ${user.tier === p.tier ? "border-cyan-500 bg-cyan-950/30" : "border-slate-200"}`}>
<div className="font-medium text-slate-800">{p.label}</div>
<div className="text-blue-600 font-bold">{p.price}</div>
<ul className="space-y-1">
{p.features.map(f => <li key={f} className="text-slate-400 text-xs"> {f}</li>)}
</ul>
{user.tier !== p.tier && p.tier !== "free" && (
<button className="w-full mt-2 bg-slate-700 hover:bg-slate-600 text-slate-800 py-1 rounded text-xs">
</button>
)}
</div>
))}
</div>
</div>
</div>
);
}