feat: full light theme - white bg, blue accent, responsive navbar
This commit is contained in:
parent
437cc35472
commit
93043009ac
@ -2,19 +2,19 @@ export default function AboutPage() {
|
||||
return (
|
||||
<div className="space-y-6 max-w-3xl">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100">策略说明</h1>
|
||||
<p className="text-slate-400 text-sm mt-1">资金费率套利原理与历史数据</p>
|
||||
<h1 className="text-2xl font-bold text-slate-900">策略说明</h1>
|
||||
<p className="text-slate-500 text-sm mt-1">资金费率套利原理与历史数据</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-slate-700 bg-slate-800/50 p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-cyan-400">策略原理</h2>
|
||||
<p className="text-slate-300 text-sm leading-relaxed">
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-blue-600">策略原理</h2>
|
||||
<p className="text-slate-700 text-sm leading-relaxed">
|
||||
永续合约每8小时结算一次资金费率。多头多时,多头付钱给空头(费率为正);空头多时,空头付钱给多头(费率为负)。
|
||||
</p>
|
||||
<p className="text-slate-300 text-sm leading-relaxed">
|
||||
<span className="text-cyan-400 font-medium">套利做法:</span>现货买入 + 永续做空,完全对冲币价风险,净收资金费率(USDT结算)。
|
||||
<p className="text-slate-700 text-sm leading-relaxed">
|
||||
<span className="text-blue-600 font-medium">套利做法:</span>现货买入 + 永续做空,完全对冲币价风险,净收资金费率(USDT结算)。
|
||||
</p>
|
||||
<div className="bg-slate-900/50 rounded-lg p-4 text-sm font-mono text-slate-300 space-y-1">
|
||||
<div className="bg-slate-50 rounded-lg p-4 text-sm font-mono text-slate-700 space-y-1">
|
||||
<div>买入 1 BTC 现货($96,000)</div>
|
||||
<div>做空 1 BTC 永续($96,000,1倍杠杆)</div>
|
||||
<div className="text-slate-500">───────────────────────</div>
|
||||
@ -23,25 +23,25 @@ export default function AboutPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-slate-700 bg-slate-800/50 p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-cyan-400">历史年化数据(2019-2026,露露×小周15轮验证)</h2>
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-blue-600">历史年化数据(2019-2026,露露×小周15轮验证)</h2>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-700">
|
||||
<th className="text-left py-2 text-slate-400">资产</th>
|
||||
<th className="text-right py-2 text-slate-400">全周期毛年化</th>
|
||||
<th className="text-right py-2 text-slate-400">PM净年化</th>
|
||||
<th className="text-right py-2 text-slate-400">负费率占比</th>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-2 text-slate-500">资产</th>
|
||||
<th className="text-right py-2 text-slate-500">全周期毛年化</th>
|
||||
<th className="text-right py-2 text-slate-500">PM净年化</th>
|
||||
<th className="text-right py-2 text-slate-500">负费率占比</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-slate-300">
|
||||
<tr className="border-b border-slate-700/50">
|
||||
<td className="py-2 text-cyan-400 font-medium">BTC</td>
|
||||
<tbody className="text-slate-700">
|
||||
<tr className="border-b border-slate-200/50">
|
||||
<td className="py-2 text-blue-600 font-medium">BTC</td>
|
||||
<td className="py-2 text-right">12.33%</td>
|
||||
<td className="py-2 text-right text-emerald-400 font-medium">11.67%</td>
|
||||
<td className="py-2 text-right">13.07%</td>
|
||||
</tr>
|
||||
<tr className="border-b border-slate-700/50">
|
||||
<tr className="border-b border-slate-200/50">
|
||||
<td className="py-2 text-violet-400 font-medium">ETH</td>
|
||||
<td className="py-2 text-right">14.87%</td>
|
||||
<td className="py-2 text-right text-emerald-400 font-medium">14.09%</td>
|
||||
@ -55,11 +55,11 @@ export default function AboutPage() {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p className="text-slate-500 text-xs">PM = Portfolio Margin模式,资金利用率约95%。数据来源:Binance fapi/v1/fundingRate官方API</p>
|
||||
<p className="text-slate-400 text-xs">PM = Portfolio Margin模式,资金利用率约95%。数据来源:Binance fapi/v1/fundingRate官方API</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-slate-700 bg-slate-800/50 p-6 space-y-3">
|
||||
<h2 className="text-lg font-semibold text-cyan-400">风险说明</h2>
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 space-y-3">
|
||||
<h2 className="text-lg font-semibold text-blue-600">风险说明</h2>
|
||||
<div className="space-y-2 text-sm">
|
||||
{[
|
||||
{ level: "🟡 中", risk: "市场周期", desc: "熊市年化可降至0-4%,但不亏本金" },
|
||||
@ -68,10 +68,10 @@ export default function AboutPage() {
|
||||
{ level: "🟢 低", risk: "爆仓风险", desc: "1倍杠杆+对冲,理论需BTC翻倍才触发" },
|
||||
{ level: "🟢 低", risk: "基差波动", desc: "长期持有不影响,只影响平仓时机" },
|
||||
].map((r, i) => (
|
||||
<div key={i} className="flex items-start gap-3 bg-slate-900/40 rounded-lg px-4 py-2">
|
||||
<div key={i} className="flex items-start gap-3 bg-slate-50 rounded-lg px-4 py-2">
|
||||
<span className="text-xs w-12 shrink-0 mt-0.5">{r.level}</span>
|
||||
<span className="text-slate-300 font-medium w-24 shrink-0">{r.risk}</span>
|
||||
<span className="text-slate-400">{r.desc}</span>
|
||||
<span className="text-slate-700 font-medium w-24 shrink-0">{r.risk}</span>
|
||||
<span className="text-slate-500">{r.desc}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -41,36 +41,36 @@ export default function DashboardPage() {
|
||||
|
||||
const logout = () => { localStorage.removeItem("arb_token"); router.push("/"); };
|
||||
|
||||
if (!user) return <div className="text-slate-400 p-8">加载中...</div>;
|
||||
if (!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-100">我的账户</h1>
|
||||
<button onClick={logout} className="text-sm text-slate-400 hover:text-slate-200">退出</button>
|
||||
<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-700 bg-slate-800/50 p-6 space-y-3">
|
||||
<h2 className="text-slate-200 font-semibold">账户信息</h2>
|
||||
<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-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 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-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="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-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-slate-100 text-sm focus:outline-none focus:border-cyan-500"
|
||||
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
|
||||
@ -81,25 +81,25 @@ export default function DashboardPage() {
|
||||
</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>
|
||||
<p className="text-slate-400 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="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-600"}`}>
|
||||
<div className="font-medium text-slate-200">{p.label}</div>
|
||||
<div className="text-cyan-400 font-bold">{p.price}</div>
|
||||
<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-200 py-1 rounded text-xs">
|
||||
<button className="w-full mt-2 bg-slate-700 hover:bg-slate-600 text-slate-800 py-1 rounded text-xs">
|
||||
升级(即将开放)
|
||||
</button>
|
||||
)}
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #0f172a;
|
||||
--foreground: #e2e8f0;
|
||||
--card: #1e293b;
|
||||
--card-foreground: #e2e8f0;
|
||||
--border: #334155;
|
||||
--muted: #475569;
|
||||
--cyan: #06b6d4;
|
||||
--background: #ffffff;
|
||||
--foreground: #0f172a;
|
||||
--card: #f8fafc;
|
||||
--card-foreground: #0f172a;
|
||||
--border: #e2e8f0;
|
||||
--muted: #64748b;
|
||||
--primary: #2563eb;
|
||||
--primary-foreground: #ffffff;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
|
||||
@ -37,16 +37,16 @@ export default function HistoryPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100">历史费率</h1>
|
||||
<p className="text-slate-400 text-sm mt-1">过去7天 BTC / ETH 资金费率记录</p>
|
||||
<h1 className="text-2xl font-bold text-slate-900">历史费率</h1>
|
||||
<p className="text-slate-500 text-sm mt-1">过去7天 BTC / ETH 资金费率记录</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-slate-400">加载中...</div>
|
||||
<div className="text-slate-500">加载中...</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="rounded-xl border border-slate-700 bg-slate-800/50 p-6">
|
||||
<h2 className="text-slate-200 font-semibold mb-4">费率走势(过去7天)</h2>
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6">
|
||||
<h2 className="text-slate-800 font-semibold mb-4">费率走势(过去7天)</h2>
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
<LineChart data={chartData} margin={{ top: 4, right: 8, bottom: 4, left: 8 }}>
|
||||
<XAxis dataKey="time" tick={{ fill: "#64748b", fontSize: 10 }} tickLine={false} interval="preserveStartEnd" />
|
||||
@ -60,19 +60,19 @@ export default function HistoryPage() {
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-slate-700 bg-slate-800/50 overflow-hidden">
|
||||
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-700 bg-slate-800">
|
||||
<th className="text-left px-4 py-3 text-slate-400 font-medium">时间</th>
|
||||
<th className="text-right px-4 py-3 text-cyan-400 font-medium">BTC 费率</th>
|
||||
<tr className="border-b border-slate-200 bg-slate-800">
|
||||
<th className="text-left px-4 py-3 text-slate-500 font-medium">时间</th>
|
||||
<th className="text-right px-4 py-3 text-blue-600 font-medium">BTC 费率</th>
|
||||
<th className="text-right px-4 py-3 text-violet-400 font-medium">ETH 费率</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableData.map((row, i) => (
|
||||
<tr key={i} className="border-b border-slate-700/50 hover:bg-slate-700/30">
|
||||
<td className="px-4 py-2 text-slate-400 font-mono text-xs">{row.time}</td>
|
||||
<tr key={i} className="border-b border-slate-200/50 hover:bg-slate-50">
|
||||
<td className="px-4 py-2 text-slate-500 font-mono text-xs">{row.time}</td>
|
||||
<td className={`px-4 py-2 text-right font-mono text-xs ${row.btc == null ? "text-slate-500" : row.btc >= 0 ? "text-emerald-400" : "text-red-400"}`}>
|
||||
{row.btc != null ? `${row.btc.toFixed(4)}%` : "--"}
|
||||
</td>
|
||||
|
||||
@ -3,31 +3,18 @@ import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Navbar from "@/components/Navbar";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
|
||||
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Arbitrage Engine",
|
||||
description: "Funding rate arbitrage monitoring system",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="zh" className="dark">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen bg-slate-900 text-slate-100`}
|
||||
>
|
||||
<html lang="zh">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen bg-white text-slate-900`}>
|
||||
<Navbar />
|
||||
<main className="max-w-6xl mx-auto px-4 py-6">{children}</main>
|
||||
</body>
|
||||
|
||||
@ -36,28 +36,28 @@ function LoginForm() {
|
||||
|
||||
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 className="w-full max-w-md rounded-xl border border-slate-200 bg-white p-8 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100">登录</h1>
|
||||
<h1 className="text-2xl font-bold text-slate-900">登录</h1>
|
||||
{params.get("registered") && (
|
||||
<p className="text-emerald-400 text-sm mt-1">✅ 注册成功,请登录</p>
|
||||
)}
|
||||
<p className="text-slate-400 text-sm mt-1">登录后查看信号和账户信息</p>
|
||||
<p className="text-slate-500 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>
|
||||
<label className="block text-sm text-slate-700 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"
|
||||
className="w-full 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="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-slate-300 mb-1">密码</label>
|
||||
<label className="block text-sm text-slate-700 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"
|
||||
className="w-full bg-white border border-slate-200 rounded-lg px-3 py-2 text-slate-900 text-sm focus:outline-none focus:border-cyan-500"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-400 text-sm">{error}</p>}
|
||||
@ -68,8 +68,8 @@ function LoginForm() {
|
||||
{loading ? "登录中..." : "登录"}
|
||||
</button>
|
||||
</form>
|
||||
<p className="text-center text-sm text-slate-400">
|
||||
没有账号?<a href="/register" className="text-cyan-400 hover:underline">注册</a>
|
||||
<p className="text-center text-sm text-slate-500">
|
||||
没有账号?<a href="/register" className="text-blue-600 hover:underline">注册</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -37,8 +37,8 @@ export default function Dashboard() {
|
||||
useEffect(() => {
|
||||
fetchRates();
|
||||
fetchAll();
|
||||
const rateInterval = setInterval(fetchRates, 2_000); // 价格 2秒刷新
|
||||
const slowInterval = setInterval(fetchAll, 120_000); // 统计 2分钟刷新
|
||||
const rateInterval = setInterval(fetchRates, 2_000);
|
||||
const slowInterval = setInterval(fetchAll, 120_000);
|
||||
return () => { clearInterval(rateInterval); clearInterval(slowInterval); };
|
||||
}, [fetchRates, fetchAll]);
|
||||
|
||||
@ -47,25 +47,18 @@ export default function Dashboard() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100">资金费率套利监控</h1>
|
||||
<p className="text-slate-400 text-sm mt-1">实时监控 BTC / ETH 永续合约资金费率</p>
|
||||
<h1 className="text-2xl font-bold text-slate-900">资金费率套利监控</h1>
|
||||
<p className="text-slate-500 text-sm mt-1">实时监控 BTC / ETH 永续合约资金费率</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
status === "running"
|
||||
? "bg-emerald-400 animate-pulse"
|
||||
: status === "error"
|
||||
? "bg-red-400"
|
||||
: "bg-slate-500"
|
||||
}`}
|
||||
/>
|
||||
<span className={status === "running" ? "text-emerald-400" : status === "error" ? "text-red-400" : "text-slate-400"}>
|
||||
<span className={`w-2 h-2 rounded-full ${
|
||||
status === "running" ? "bg-emerald-500 animate-pulse"
|
||||
: status === "error" ? "bg-red-500" : "bg-slate-300"
|
||||
}`} />
|
||||
<span className={status === "running" ? "text-emerald-600" : status === "error" ? "text-red-600" : "text-slate-400"}>
|
||||
{status === "running" ? "运行中" : status === "error" ? "连接失败" : "加载中..."}
|
||||
</span>
|
||||
{lastUpdate && (
|
||||
<span className="text-slate-500 ml-2">更新于 {lastUpdate}</span>
|
||||
)}
|
||||
{lastUpdate && <span className="text-slate-400 ml-2">更新于 {lastUpdate}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -78,38 +71,23 @@ export default function Dashboard() {
|
||||
{/* Stats Cards */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<StatsCard
|
||||
title="BTC 套利"
|
||||
mean7d={stats.BTC.mean7d}
|
||||
annualized={stats.BTC.annualized}
|
||||
accent="cyan"
|
||||
/>
|
||||
<StatsCard
|
||||
title="ETH 套利"
|
||||
mean7d={stats.ETH.mean7d}
|
||||
annualized={stats.ETH.annualized}
|
||||
accent="violet"
|
||||
/>
|
||||
<StatsCard
|
||||
title="50/50 组合"
|
||||
mean7d={stats.combo.mean7d}
|
||||
annualized={stats.combo.annualized}
|
||||
accent="emerald"
|
||||
/>
|
||||
<StatsCard title="BTC 套利" mean7d={stats.BTC.mean7d} annualized={stats.BTC.annualized} accent="blue" />
|
||||
<StatsCard title="ETH 套利" mean7d={stats.ETH.mean7d} annualized={stats.ETH.annualized} accent="indigo" />
|
||||
<StatsCard title="50/50 组合" mean7d={stats.combo.mean7d} annualized={stats.combo.annualized} accent="green" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* History Chart */}
|
||||
{history && (
|
||||
<div className="rounded-xl border border-slate-700 bg-slate-800/50 p-6">
|
||||
<h2 className="text-slate-200 font-semibold mb-4">过去7天资金费率走势</h2>
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6">
|
||||
<h2 className="text-slate-800 font-semibold mb-4">过去7天资金费率走势</h2>
|
||||
<FundingChart history={history} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Strategy note */}
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-800/30 px-5 py-3 text-sm text-slate-400">
|
||||
<span className="text-cyan-400 font-medium">策略原理:</span>
|
||||
<div className="rounded-lg border border-blue-100 bg-blue-50 px-5 py-3 text-sm text-slate-600">
|
||||
<span className="text-blue-600 font-medium">策略原理:</span>
|
||||
持有现货多头 + 永续空头,每8小时收取资金费率,赚取无方向风险的稳定收益。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -32,34 +32,34 @@ export default function RegisterPage() {
|
||||
|
||||
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 className="w-full max-w-md rounded-xl border border-slate-200 bg-white 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>
|
||||
<h1 className="text-2xl font-bold text-slate-900">注册账号</h1>
|
||||
<p className="text-slate-500 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>
|
||||
<label className="block text-sm text-slate-700 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"
|
||||
className="w-full 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="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-slate-300 mb-1">密码</label>
|
||||
<label className="block text-sm text-slate-700 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"
|
||||
className="w-full 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="至少8位"
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-slate-300 mb-1">Discord ID <span className="text-slate-500">(选填,用于接收信号)</span></label>
|
||||
<label className="block text-sm text-slate-700 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"
|
||||
className="w-full 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="例:123456789012345678"
|
||||
/>
|
||||
</div>
|
||||
@ -71,8 +71,8 @@ export default function RegisterPage() {
|
||||
{loading ? "注册中..." : "注册"}
|
||||
</button>
|
||||
</form>
|
||||
<p className="text-center text-sm text-slate-400">
|
||||
已有账号?<a href="/login" className="text-cyan-400 hover:underline">登录</a>
|
||||
<p className="text-center text-sm text-slate-500">
|
||||
已有账号?<a href="/login" className="text-blue-600 hover:underline">登录</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
61
frontend/app/signals/page.tsx
Normal file
61
frontend/app/signals/page.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { api, SignalHistoryItem } from "@/lib/api";
|
||||
|
||||
export default function SignalsPage() {
|
||||
const [items, setItems] = useState<SignalHistoryItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
try {
|
||||
const data = await api.signalsHistory();
|
||||
setItems(data.items || []);
|
||||
} catch {
|
||||
setError("加载信号历史失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
run();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-bold text-slate-900">信号历史</h1>
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-4 overflow-x-auto">
|
||||
{loading ? <p className="text-slate-400">加载中...</p> : null}
|
||||
{error ? <p className="text-red-500">{error}</p> : null}
|
||||
{!loading && !error ? (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-slate-500 border-b border-slate-200">
|
||||
<th className="text-left py-2">时间</th>
|
||||
<th className="text-left py-2">币种</th>
|
||||
<th className="text-left py-2">年化</th>
|
||||
<th className="text-left py-2">信号类型</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((row) => (
|
||||
<tr key={row.id} className="border-b border-slate-100 text-slate-700">
|
||||
<td className="py-2">{new Date(row.sent_at).toLocaleString("zh-CN")}</td>
|
||||
<td className="py-2 font-medium">{row.symbol}</td>
|
||||
<td className="py-2 text-blue-600 font-mono">{row.annualized}%</td>
|
||||
<td className="py-2 text-slate-500">资金费率套利信号</td>
|
||||
</tr>
|
||||
))}
|
||||
{!items.length ? (
|
||||
<tr>
|
||||
<td className="py-3 text-slate-400" colSpan={4}>暂无信号记录(费率超10%时自动触发)</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -13,34 +13,30 @@ export default function Navbar() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<nav className="border-b border-slate-700 bg-slate-900/80 backdrop-blur sticky top-0 z-50">
|
||||
<nav className="border-b border-slate-200 bg-white sticky top-0 z-50 shadow-sm">
|
||||
<div className="max-w-6xl mx-auto px-4 h-14 flex items-center">
|
||||
<Link href="/" className="font-bold text-cyan-400 tracking-tight text-lg shrink-0">
|
||||
<Link href="/" className="font-bold text-blue-600 tracking-tight text-lg shrink-0">
|
||||
⚡ Arbitrage Engine
|
||||
</Link>
|
||||
|
||||
{/* Desktop nav */}
|
||||
<div className="hidden md:flex items-center gap-6 text-sm ml-8">
|
||||
{navLinks.map(l => (
|
||||
<Link key={l.href} href={l.href} className="text-slate-300 hover:text-cyan-400 transition-colors">
|
||||
<Link key={l.href} href={l.href} className="text-slate-600 hover:text-blue-600 transition-colors">
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden md:flex items-center gap-3 text-sm ml-auto">
|
||||
<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>
|
||||
<Link href="/login" className="text-slate-500 hover:text-slate-800 transition-colors">登录</Link>
|
||||
<Link href="/register" className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg transition-colors">注册</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile: right side buttons + hamburger */}
|
||||
{/* Mobile */}
|
||||
<div className="md:hidden ml-auto flex items-center gap-2">
|
||||
<Link href="/login" className="text-slate-400 hover:text-slate-200 text-sm transition-colors">登录</Link>
|
||||
<Link href="/register" className="bg-cyan-600 hover:bg-cyan-500 text-white px-2 py-1 rounded text-sm transition-colors">注册</Link>
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="ml-1 p-2 text-slate-400 hover:text-slate-200"
|
||||
aria-label="菜单"
|
||||
>
|
||||
<Link href="/login" className="text-slate-500 text-sm">登录</Link>
|
||||
<Link href="/register" className="bg-blue-600 text-white px-2 py-1 rounded text-sm">注册</Link>
|
||||
<button onClick={() => setOpen(!open)} className="ml-1 p-2 text-slate-500" aria-label="菜单">
|
||||
{open ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
@ -54,16 +50,11 @@ export default function Navbar() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile dropdown */}
|
||||
{open && (
|
||||
<div className="md:hidden border-t border-slate-700 bg-slate-900 px-4 py-3 space-y-1">
|
||||
<div className="md:hidden border-t border-slate-100 bg-white px-4 py-3 space-y-1">
|
||||
{navLinks.map(l => (
|
||||
<Link
|
||||
key={l.href}
|
||||
href={l.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className="block py-2 text-slate-300 hover:text-cyan-400 transition-colors text-sm"
|
||||
>
|
||||
<Link key={l.href} href={l.href} onClick={() => setOpen(false)}
|
||||
className="block py-2 text-slate-600 hover:text-blue-600 transition-colors text-sm">
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@ -12,23 +12,23 @@ const ASSET_EMOJI: Record<string, string> = { BTC: "₿", ETH: "Ξ" };
|
||||
export default function RateCard({ asset, data }: Props) {
|
||||
const rate = data?.lastFundingRate ?? null;
|
||||
const positive = rate !== null && rate >= 0;
|
||||
const rateColor = rate === null ? "text-slate-400" : positive ? "text-emerald-400" : "text-red-400";
|
||||
const rateColor = rate === null ? "text-slate-400" : positive ? "text-emerald-600" : "text-red-500";
|
||||
const badgeColor = rate === null
|
||||
? "bg-slate-700 text-slate-300"
|
||||
? "bg-slate-100 text-slate-500"
|
||||
: positive
|
||||
? "bg-emerald-500/20 text-emerald-300 border border-emerald-500/30"
|
||||
: "bg-red-500/20 text-red-300 border border-red-500/30";
|
||||
? "bg-emerald-50 text-emerald-700 border border-emerald-200"
|
||||
: "bg-red-50 text-red-600 border border-red-200";
|
||||
|
||||
const nextTime = data?.nextFundingTime
|
||||
? new Date(data.nextFundingTime).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
|
||||
: "--";
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-700 bg-slate-800/60 p-6 space-y-4">
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl font-bold text-slate-200">{ASSET_EMOJI[asset]}</span>
|
||||
<span className="text-lg font-semibold text-slate-200">{asset}/USDT</span>
|
||||
<span className="text-2xl font-bold text-slate-700">{ASSET_EMOJI[asset]}</span>
|
||||
<span className="text-lg font-semibold text-slate-800">{asset}/USDT</span>
|
||||
</div>
|
||||
<span className={`text-xs px-2 py-1 rounded-full font-medium ${badgeColor}`}>
|
||||
{rate === null ? "加载中" : positive ? "正费率 收款" : "负费率 付款"}
|
||||
@ -42,16 +42,16 @@ export default function RateCard({ asset, data }: Props) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 pt-2 border-t border-slate-700">
|
||||
<div className="grid grid-cols-2 gap-3 pt-2 border-t border-slate-100">
|
||||
<div>
|
||||
<p className="text-slate-500 text-xs">标记价格</p>
|
||||
<p className="text-slate-200 font-mono text-sm mt-0.5">
|
||||
<p className="text-slate-400 text-xs">标记价格</p>
|
||||
<p className="text-slate-700 font-mono text-sm mt-0.5">
|
||||
${data ? Number(data.markPrice).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : "--"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-500 text-xs">下次结算</p>
|
||||
<p className="text-cyan-400 font-mono text-sm mt-0.5">{nextTime}</p>
|
||||
<p className="text-slate-400 text-xs">下次结算</p>
|
||||
<p className="text-blue-600 font-mono text-sm mt-0.5">{nextTime}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,27 +4,30 @@ interface Props {
|
||||
title: string;
|
||||
mean7d: number;
|
||||
annualized: number;
|
||||
accent: "cyan" | "violet" | "emerald";
|
||||
accent: "blue" | "indigo" | "green";
|
||||
}
|
||||
|
||||
const accentMap = {
|
||||
cyan: {
|
||||
border: "border-cyan-500/30",
|
||||
bg: "bg-cyan-500/10",
|
||||
text: "text-cyan-400",
|
||||
label: "text-cyan-300",
|
||||
blue: {
|
||||
border: "border-blue-200",
|
||||
bg: "bg-blue-50",
|
||||
text: "text-blue-600",
|
||||
label: "text-blue-700",
|
||||
divider: "border-blue-100",
|
||||
},
|
||||
violet: {
|
||||
border: "border-violet-500/30",
|
||||
bg: "bg-violet-500/10",
|
||||
text: "text-violet-400",
|
||||
label: "text-violet-300",
|
||||
indigo: {
|
||||
border: "border-indigo-200",
|
||||
bg: "bg-indigo-50",
|
||||
text: "text-indigo-600",
|
||||
label: "text-indigo-700",
|
||||
divider: "border-indigo-100",
|
||||
},
|
||||
emerald: {
|
||||
border: "border-emerald-500/30",
|
||||
bg: "bg-emerald-500/10",
|
||||
text: "text-emerald-400",
|
||||
label: "text-emerald-300",
|
||||
green: {
|
||||
border: "border-green-200",
|
||||
bg: "bg-green-50",
|
||||
text: "text-green-600",
|
||||
label: "text-green-700",
|
||||
divider: "border-green-100",
|
||||
},
|
||||
};
|
||||
|
||||
@ -38,12 +41,12 @@ export default function StatsCard({ title, mean7d, annualized, accent }: Props)
|
||||
|
||||
<div>
|
||||
<p className="text-slate-400 text-xs">预估年化收益率</p>
|
||||
<p className={`text-2xl font-bold font-mono mt-0.5 ${isPositive ? c.text : "text-red-400"}`}>
|
||||
<p className={`text-2xl font-bold font-mono mt-0.5 ${isPositive ? c.text : "text-red-500"}`}>
|
||||
{annualized >= 0 ? "+" : ""}{annualized.toFixed(2)}%
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-slate-700/50">
|
||||
<div className={`pt-2 border-t ${c.divider}`}>
|
||||
<p className="text-slate-400 text-xs">7天均值费率</p>
|
||||
<p className={`text-sm font-mono mt-0.5 ${c.text}`}>
|
||||
{mean7d >= 0 ? "+" : ""}{mean7d.toFixed(4)}% / 8h
|
||||
|
||||
Loading…
Reference in New Issue
Block a user