197 lines
7.7 KiB
TypeScript
197 lines
7.7 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState, useCallback } from "react";
|
||
import { authFetch, useAuth } from "@/lib/auth";
|
||
import Link from "next/link";
|
||
import {
|
||
TrendingUp, TrendingDown, Clock, Activity, RotateCcw
|
||
} from "lucide-react";
|
||
|
||
interface DeprecatedStrategy {
|
||
strategy_id: string;
|
||
display_name: string;
|
||
symbol: string;
|
||
status: string;
|
||
started_at: number;
|
||
deprecated_at: number | null;
|
||
initial_balance: number;
|
||
current_balance: number;
|
||
net_usdt: number;
|
||
net_r: number;
|
||
trade_count: number;
|
||
win_rate: number;
|
||
avg_win_r: number;
|
||
avg_loss_r: number;
|
||
pnl_usdt_24h: number;
|
||
last_trade_at: number | null;
|
||
}
|
||
|
||
function formatTime(ms: number | null): string {
|
||
if (!ms) return "—";
|
||
return new Date(ms).toLocaleString("zh-CN", {
|
||
timeZone: "Asia/Shanghai",
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
hour12: false,
|
||
});
|
||
}
|
||
|
||
export default function DeprecatedStrategiesPage() {
|
||
useAuth();
|
||
const [strategies, setStrategies] = useState<DeprecatedStrategy[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [restoring, setRestoring] = useState<string | null>(null);
|
||
|
||
const fetchData = useCallback(async () => {
|
||
try {
|
||
const res = await authFetch("/api/strategies?include_deprecated=true");
|
||
const data = await res.json();
|
||
const deprecated = (data.strategies || []).filter(
|
||
(s: DeprecatedStrategy) => s.status === "deprecated"
|
||
);
|
||
setStrategies(deprecated);
|
||
} catch (e) {
|
||
console.error(e);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
fetchData();
|
||
}, [fetchData]);
|
||
|
||
const handleRestore = async (sid: string, name: string) => {
|
||
if (!confirm(`确认重新启用策略「${name}」?将继续使用原有余额和历史数据。`)) return;
|
||
setRestoring(sid);
|
||
try {
|
||
await authFetch(`/api/strategies/${sid}/restore`, { method: "POST" });
|
||
await fetchData();
|
||
} catch (e) {
|
||
console.error(e);
|
||
} finally {
|
||
setRestoring(null);
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex items-center justify-center min-h-64">
|
||
<div className="text-slate-400 text-sm animate-pulse">加载中...</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="p-4 max-w-5xl mx-auto">
|
||
<div className="flex items-center justify-between mb-5">
|
||
<div>
|
||
<h1 className="text-lg font-bold text-slate-800">废弃策略</h1>
|
||
<p className="text-slate-500 text-xs mt-0.5">数据永久保留,可随时重新启用</p>
|
||
</div>
|
||
<Link
|
||
href="/strategy-plaza"
|
||
className="text-xs text-blue-600 hover:underline"
|
||
>
|
||
← 返回策略广场
|
||
</Link>
|
||
</div>
|
||
|
||
{strategies.length === 0 ? (
|
||
<div className="text-center text-slate-400 text-sm py-16">暂无废弃策略</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||
{strategies.map((s) => {
|
||
const isProfit = s.net_usdt >= 0;
|
||
const is24hProfit = s.pnl_usdt_24h >= 0;
|
||
const balancePct = ((s.current_balance / s.initial_balance) * 100).toFixed(1);
|
||
return (
|
||
<div key={s.strategy_id} className="rounded-xl border border-slate-200 bg-white overflow-hidden opacity-80">
|
||
{/* Header */}
|
||
<div className="px-4 py-3 border-b border-slate-100 flex items-center justify-between bg-slate-50">
|
||
<div className="flex items-center gap-2">
|
||
<h3 className="font-semibold text-slate-600 text-sm">{s.display_name}</h3>
|
||
<span className="text-[10px] text-slate-400 bg-slate-200 px-1.5 py-0.5 rounded-full">已废弃</span>
|
||
</div>
|
||
<span className="text-[10px] text-slate-400">{s.symbol.replace("USDT", "")}</span>
|
||
</div>
|
||
|
||
{/* PnL */}
|
||
<div className="px-4 pt-3 pb-2">
|
||
<div className="flex items-end justify-between mb-2">
|
||
<div>
|
||
<div className="text-[10px] text-slate-400 mb-0.5">废弃时余额</div>
|
||
<div className="text-xl font-bold text-slate-700">
|
||
{s.current_balance.toLocaleString()}
|
||
<span className="text-xs font-normal text-slate-400 ml-1">USDT</span>
|
||
</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<div className="text-[10px] text-slate-400 mb-0.5">累计盈亏</div>
|
||
<div className={`text-lg font-bold ${isProfit ? "text-emerald-600" : "text-red-500"}`}>
|
||
{isProfit ? "+" : ""}{s.net_usdt.toLocaleString()} U
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Balance bar */}
|
||
<div className="mb-3">
|
||
<div className="flex justify-between text-[10px] text-slate-400 mb-1">
|
||
<span>{balancePct}%</span>
|
||
<span>{s.initial_balance.toLocaleString()} USDT 初始</span>
|
||
</div>
|
||
<div className="w-full bg-slate-100 rounded-full h-1.5">
|
||
<div
|
||
className={`h-1.5 rounded-full ${isProfit ? "bg-emerald-400" : "bg-red-300"}`}
|
||
style={{ width: `${Math.min(100, Math.max(0, (s.current_balance / s.initial_balance) * 100))}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stats */}
|
||
<div className="grid grid-cols-3 gap-2 mb-3">
|
||
<div className="bg-slate-50 rounded-lg p-2 text-center">
|
||
<div className="text-[10px] text-slate-400">胜率</div>
|
||
<div className="text-sm font-bold text-slate-600">{s.win_rate}%</div>
|
||
</div>
|
||
<div className="bg-slate-50 rounded-lg p-2 text-center">
|
||
<div className="text-[10px] text-slate-400">净R</div>
|
||
<div className={`text-sm font-bold ${s.net_r >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
||
{s.net_r >= 0 ? "+" : ""}{s.net_r}R
|
||
</div>
|
||
</div>
|
||
<div className="bg-slate-50 rounded-lg p-2 text-center">
|
||
<div className="text-[10px] text-slate-400">交易数</div>
|
||
<div className="text-sm font-bold text-slate-700">{s.trade_count}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<div className="px-4 py-2.5 border-t border-slate-100 flex items-center justify-between bg-slate-50/60">
|
||
<div className="flex items-center gap-1">
|
||
{is24hProfit ? <TrendingUp size={12} className="text-emerald-500" /> : <TrendingDown size={12} className="text-red-400" />}
|
||
<span className="text-[10px] text-slate-500">
|
||
废弃于 {formatTime(s.deprecated_at)}
|
||
</span>
|
||
</div>
|
||
<button
|
||
onClick={() => handleRestore(s.strategy_id, s.display_name)}
|
||
disabled={restoring === s.strategy_id}
|
||
className="flex items-center gap-1 text-[11px] px-2.5 py-1 rounded-lg bg-blue-50 text-blue-600 hover:bg-blue-100 disabled:opacity-50 transition-colors font-medium"
|
||
>
|
||
<RotateCcw size={11} />
|
||
{restoring === s.strategy_id ? "启用中..." : "重新启用"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|