feat: add register/login/dashboard pages, signals nav

This commit is contained in:
root 2026-02-26 17:05:49 +00:00
parent b1d959cf20
commit 89a390e6bd
4 changed files with 287 additions and 0 deletions

View File

@ -0,0 +1,112 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
interface UserInfo {
id: number;
email: string;
discord_id: string | null;
tier: string;
expires_at: string | null;
}
export default function DashboardPage() {
const router = useRouter();
const [user, setUser] = useState<UserInfo | null>(null);
const [discordId, setDiscordId] = useState("");
const [saving, setSaving] = useState(false);
const [msg, setMsg] = useState("");
useEffect(() => {
const token = localStorage.getItem("arb_token");
if (!token) { router.push("/login"); return; }
fetch("/api/user/me", { headers: { Authorization: `Bearer ${token}` } })
.then(r => { if (!r.ok) { router.push("/login"); return null; } return r.json(); })
.then(d => { if (d) { setUser(d); setDiscordId(d.discord_id || ""); } });
}, [router]);
const bindDiscord = async () => {
setSaving(true); setMsg("");
const token = localStorage.getItem("arb_token");
const r = await fetch("/api/user/bind-discord", {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ discord_id: discordId }),
});
const d = await r.json();
setMsg(r.ok ? "✅ 绑定成功" : d.detail || "绑定失败");
setSaving(false);
if (r.ok && user) setUser({ ...user, discord_id: discordId });
};
const logout = () => { localStorage.removeItem("arb_token"); router.push("/"); };
if (!user) return <div className="text-slate-400 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-100"></h1>
<button onClick={logout} className="text-sm text-slate-400 hover:text-slate-200">退</button>
</div>
<div className="rounded-xl border border-slate-700 bg-slate-800/50 p-6 space-y-3">
<h2 className="text-slate-200 font-semibold"></h2>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="text-slate-400"></div>
<div className="text-slate-200">{user.email}</div>
<div className="text-slate-400"></div>
<div className="text-cyan-400 font-medium">{tierLabel[user.tier] || user.tier}</div>
<div className="text-slate-400"></div>
<div className="text-slate-200">{user.expires_at ? new Date(user.expires_at).toLocaleDateString("zh-CN") : "永久免费"}</div>
</div>
</div>
<div className="rounded-xl border border-slate-700 bg-slate-800/50 p-6 space-y-4">
<h2 className="text-slate-200 font-semibold">Discord </h2>
<p className="text-slate-400 text-sm">Discord ID后@你</p>
<div className="flex gap-2">
<input
value={discordId} onChange={e => setDiscordId(e.target.value)}
className="flex-1 bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-slate-100 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("✅") ? "text-emerald-400" : "text-red-400"}`}>{msg}</p>}
<p className="text-slate-500 text-xs">Discord ID ID</p>
</div>
<div className="rounded-xl border border-slate-700 bg-slate-800/50 p-6">
<h2 className="text-slate-200 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-600"}`}>
<div className="font-medium text-slate-200">{p.label}</div>
<div className="text-cyan-400 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-200 py-1 rounded text-xs">
</button>
)}
</div>
))}
</div>
</div>
</div>
);
}

View File

@ -47,6 +47,12 @@ export default function RootLayout({
> >
</Link> </Link>
<Link
href="/signals"
className="text-slate-300 hover:text-cyan-400 transition-colors"
>
</Link>
<Link <Link
href="/about" href="/about"
className="text-slate-300 hover:text-cyan-400 transition-colors" className="text-slate-300 hover:text-cyan-400 transition-colors"
@ -54,6 +60,10 @@ export default function RootLayout({
</Link> </Link>
</div> </div>
<div className="ml-auto flex gap-3 text-sm">
<Link href="/login" className="text-slate-400 hover:text-slate-200 transition-colors"></Link>
<Link href="/register" className="bg-cyan-600 hover:bg-cyan-500 text-white px-3 py-1 rounded-lg transition-colors"></Link>
</div>
</div> </div>
</nav> </nav>

View File

@ -0,0 +1,85 @@
"use client";
import { useState, Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
function LoginForm() {
const router = useRouter();
const params = useSearchParams();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError("");
try {
const form = new URLSearchParams();
form.append("username", email);
form.append("password", password);
const r = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: form.toString(),
});
const data = await r.json();
if (!r.ok) { setError(data.detail || "登录失败"); return; }
localStorage.setItem("arb_token", data.access_token);
router.push("/dashboard");
} catch {
setError("网络错误,请重试");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-[70vh] flex items-center justify-center">
<div className="w-full max-w-md rounded-xl border border-slate-700 bg-slate-800/50 p-8 space-y-6">
<div>
<h1 className="text-2xl font-bold text-slate-100"></h1>
{params.get("registered") && (
<p className="text-emerald-400 text-sm mt-1"> </p>
)}
<p className="text-slate-400 text-sm mt-1"></p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm text-slate-300 mb-1"></label>
<input
type="email" required value={email} onChange={e => setEmail(e.target.value)}
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-slate-100 text-sm focus:outline-none focus:border-cyan-500"
placeholder="your@email.com"
/>
</div>
<div>
<label className="block text-sm text-slate-300 mb-1"></label>
<input
type="password" required value={password} onChange={e => setPassword(e.target.value)}
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-slate-100 text-sm focus:outline-none focus:border-cyan-500"
/>
</div>
{error && <p className="text-red-400 text-sm">{error}</p>}
<button
type="submit" disabled={loading}
className="w-full bg-cyan-600 hover:bg-cyan-500 disabled:opacity-50 text-white font-medium py-2 rounded-lg text-sm transition-colors"
>
{loading ? "登录中..." : "登录"}
</button>
</form>
<p className="text-center text-sm text-slate-400">
<a href="/register" className="text-cyan-400 hover:underline"></a>
</p>
</div>
</div>
);
}
export default function LoginPage() {
return (
<Suspense>
<LoginForm />
</Suspense>
);
}

View File

@ -0,0 +1,80 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function RegisterPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [discordId, setDiscordId] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError("");
try {
const r = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password, discord_id: discordId || undefined }),
});
const data = await r.json();
if (!r.ok) { setError(data.detail || "注册失败"); return; }
router.push("/login?registered=1");
} catch {
setError("网络错误,请重试");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-[70vh] flex items-center justify-center">
<div className="w-full max-w-md rounded-xl border border-slate-700 bg-slate-800/50 p-8 space-y-6">
<div>
<h1 className="text-2xl font-bold text-slate-100"></h1>
<p className="text-slate-400 text-sm mt-1"></p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm text-slate-300 mb-1"></label>
<input
type="email" required value={email} onChange={e => setEmail(e.target.value)}
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-slate-100 text-sm focus:outline-none focus:border-cyan-500"
placeholder="your@email.com"
/>
</div>
<div>
<label className="block text-sm text-slate-300 mb-1"></label>
<input
type="password" required value={password} onChange={e => setPassword(e.target.value)}
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-slate-100 text-sm focus:outline-none focus:border-cyan-500"
placeholder="至少8位"
minLength={8}
/>
</div>
<div>
<label className="block text-sm text-slate-300 mb-1">Discord ID <span className="text-slate-500"></span></label>
<input
type="text" value={discordId} onChange={e => setDiscordId(e.target.value)}
className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-slate-100 text-sm focus:outline-none focus:border-cyan-500"
placeholder="例123456789012345678"
/>
</div>
{error && <p className="text-red-400 text-sm">{error}</p>}
<button
type="submit" disabled={loading}
className="w-full bg-cyan-600 hover:bg-cyan-500 disabled:opacity-50 text-white font-medium py-2 rounded-lg text-sm transition-colors"
>
{loading ? "注册中..." : "注册"}
</button>
</form>
<p className="text-center text-sm text-slate-400">
<a href="/login" className="text-cyan-400 hover:underline"></a>
</p>
</div>
</div>
);
}