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)
124 lines
5.4 KiB
TypeScript
124 lines
5.4 KiB
TypeScript
"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用户ID(18位数字)"
|
||
/>
|
||
<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>
|
||
);
|
||
}
|