diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx new file mode 100644 index 0000000..289ee66 --- /dev/null +++ b/frontend/app/dashboard/page.tsx @@ -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(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
加载中...
; + + const tierLabel: Record = { free: "免费版", pro: "Pro", premium: "Premium" }; + + return ( +
+
+

我的账户

+ +
+ +
+

账户信息

+
+
邮箱
+
{user.email}
+
订阅等级
+
{tierLabel[user.tier] || user.tier}
+
到期时间
+
{user.expires_at ? new Date(user.expires_at).toLocaleDateString("zh-CN") : "永久免费"}
+
+
+ +
+

Discord 信号推送

+

绑定Discord ID后,当套利信号触发时会自动@你

+
+ 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用户ID(18位数字)" + /> + +
+ {msg &&

{msg}

} +

如何获取Discord ID:设置 → 外观 → 开发者模式 → 右键个人头像 → 复制用户ID

+
+ +
+

升级订阅

+
+ {[ + { tier: "free", label: "免费版", price: "¥0", features: ["实时费率面板"] }, + { tier: "pro", label: "Pro", price: "¥99/月", features: ["实时费率面板", "信号Discord推送", "历史数据"] }, + { tier: "premium", label: "Premium", price: "¥299/月", features: ["Pro全部功能", "定制阈值", "优先客服"] }, + ].map(p => ( +
+
{p.label}
+
{p.price}
+
    + {p.features.map(f =>
  • • {f}
  • )} +
+ {user.tier !== p.tier && p.tier !== "free" && ( + + )} +
+ ))} +
+
+
+ ); +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 1f941ab..7ebb7ca 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -47,6 +47,12 @@ export default function RootLayout({ > 历史 + + 信号 + +
+ 登录 + 注册 +
diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx new file mode 100644 index 0000000..8324b13 --- /dev/null +++ b/frontend/app/login/page.tsx @@ -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 ( +
+
+
+

登录

+ {params.get("registered") && ( +

✅ 注册成功,请登录

+ )} +

登录后查看信号和账户信息

+
+
+
+ + 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" + /> +
+
+ + 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" + /> +
+ {error &&

{error}

} + +
+

+ 没有账号?注册 +

+
+
+ ); +} + +export default function LoginPage() { + return ( + + + + ); +} diff --git a/frontend/app/register/page.tsx b/frontend/app/register/page.tsx new file mode 100644 index 0000000..a4e57c7 --- /dev/null +++ b/frontend/app/register/page.tsx @@ -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 ( +
+
+
+

注册账号

+

注册后可接收套利信号推送

+
+
+
+ + 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" + /> +
+
+ + 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} + /> +
+
+ + 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" + /> +
+ {error &&

{error}

} + +
+

+ 已有账号?登录 +

+
+
+ ); +}