remove frontend directory from git tracking
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, CalendarDays, LineChart as LineChartIcon, ShieldCheck } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { PublicHeader } from "@/components/public-header";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import type { FundProduct, FundProductList } from "@/types/api";
|
||||
|
||||
interface HistoryPoint { nav_date: string; unit_nav: number; daily_growth?: number | null; cum_return_pct?: number; }
|
||||
interface HistoryResponse { items: HistoryPoint[]; total_growth_rate: number; max_return_rate: number; max_drawdown_rate: number; }
|
||||
|
||||
export default function FundDetailPage({ params }: { params: Promise<{ code: string }> }) {
|
||||
const [product, setProduct] = useState<FundProduct | null>(null);
|
||||
const [history, setHistory] = useState<HistoryResponse | null>(null);
|
||||
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
|
||||
|
||||
useEffect(() => {
|
||||
void params.then(({ code }) => Promise.all([
|
||||
apiFetch<FundProductList>(`/products?keyword=${encodeURIComponent(code)}&page_size=10`),
|
||||
apiFetch<HistoryResponse>(`/products/${encodeURIComponent(code)}/history`),
|
||||
]).then(([list, chart]) => {
|
||||
setProduct(list.items.find((item) => item.product_code === code) ?? null);
|
||||
setHistory(chart); setState("ready");
|
||||
}).catch(() => setState("error")));
|
||||
}, [params]);
|
||||
|
||||
return <main className="min-h-screen bg-slate-50 text-slate-900"><PublicHeader /><div className="mx-auto max-w-7xl px-8 pb-24 pt-32"><Link href="/products" className="inline-flex items-center gap-2 text-sm text-slate-500 hover:text-[var(--brand-primary)]"><ArrowLeft className="h-4 w-4" />返回基金列表</Link>{state === "loading" && <div className="mt-10 h-96 animate-pulse rounded-2xl bg-slate-200" />}{state === "error" && <Notice text="基金详情暂时无法加载,请稍后重试。" />}{state === "ready" && product && <><div className="mt-10 rounded-2xl bg-[var(--brand-navy)] p-10 text-white"><div className="flex flex-wrap items-start justify-between gap-8"><div><span className="rounded-full bg-white/10 px-3 py-1 text-xs text-blue-100">{product.risk_level}</span><h1 className="mt-5 text-4xl font-semibold">{product.product_name}</h1><p className="mt-2 text-sm text-blue-100">{product.product_code} · {product.product_type}</p></div><div className="text-right"><p className="text-xs text-blue-200">最新单位净值</p><p className="mt-2 text-4xl font-semibold">{product.nav ?? "--"}</p><p className="mt-2 text-xs text-blue-200">更新于 {product.nav_date ?? "暂无日期"}</p></div></div></div><div className="mt-6 grid gap-5 md:grid-cols-3"><Metric label="区间收益" value={history?.total_growth_rate} tone="green" /><Metric label="区间最大收益" value={history?.max_return_rate} tone="gold" /><Metric label="最大回撤" value={history?.max_drawdown_rate} tone="red" /></div><div className="mt-6 rounded-2xl border border-slate-200 bg-white p-8"><div className="flex items-center justify-between"><div><h2 className="flex items-center gap-2 text-lg font-semibold text-[var(--brand-navy)]"><LineChartIcon className="h-5 w-5 text-[var(--brand-primary)]" />历史净值走势</h2><p className="mt-1 text-sm text-slate-400">数据来自基金历史净值接口 · {history?.items.length ?? 0} 个数据点</p></div><CalendarDays className="h-5 w-5 text-slate-300" /></div><NavLineChart points={history?.items ?? []} /><p className="mt-4 flex items-center gap-2 text-xs text-slate-400"><ShieldCheck className="h-4 w-4" />基金投资有风险,过往业绩不代表未来表现。</p></div></>}{state === "ready" && !product && <Notice text="未找到该基金产品。" />}</div></main>;
|
||||
}
|
||||
|
||||
function Metric({ label, value, tone }: { label: string; value?: number; tone: "green" | "gold" | "red" }) { const color = tone === "green" ? "text-emerald-600" : tone === "gold" ? "text-[var(--brand-gold)]" : "text-rose-600"; return <div className="rounded-2xl border border-slate-200 bg-white p-6"><p className="text-xs text-slate-400">{label}</p><p className={`mt-3 text-2xl font-semibold ${color}`}>{value ?? "--"}{value !== undefined && "%"}</p></div>; }
|
||||
|
||||
function NavLineChart({ points }: { points: HistoryPoint[] }) {
|
||||
const chartPoints = useMemo(() => points.slice(-60), [points]);
|
||||
if (!chartPoints.length) return <div className="mt-8 flex h-64 items-center justify-center rounded-xl bg-slate-50 text-sm text-slate-400">暂无历史净值数据</div>;
|
||||
const values = chartPoints.map((point) => point.unit_nav); const min = Math.min(...values); const max = Math.max(...values); const range = Math.max(max - min, 0.000001); const coords = chartPoints.map((point, index) => { const x = (index / Math.max(chartPoints.length - 1, 1)) * 100; const y = 92 - ((point.unit_nav - min) / range) * 76; return `${x},${y}`; }).join(" ");
|
||||
return <div className="mt-8"><div className="relative h-64 overflow-hidden rounded-xl border border-slate-100 bg-slate-50 px-4 py-5"><svg viewBox="0 0 100 100" preserveAspectRatio="none" className="h-full w-full" role="img" aria-label="基金历史净值折线图"><line x1="0" y1="16" x2="100" y2="16" stroke="#e2e8f0" strokeWidth="0.4" /><line x1="0" y1="54" x2="100" y2="54" stroke="#e2e8f0" strokeWidth="0.4" /><line x1="0" y1="92" x2="100" y2="92" stroke="#cbd5e1" strokeWidth="0.5" /><polyline points={coords} fill="none" stroke="var(--brand-primary)" strokeWidth="1.2" vectorEffect="non-scaling-stroke" strokeLinecap="round" strokeLinejoin="round" /></svg><div className="pointer-events-none absolute inset-x-4 bottom-2 flex justify-between text-[10px] text-slate-400"><span>{chartPoints[0].nav_date}</span><span>{chartPoints[chartPoints.length - 1].nav_date}</span></div></div><div className="mt-3 flex justify-between text-xs text-slate-400"><span>最低净值 {min.toFixed(4)}</span><span>最高净值 {max.toFixed(4)}</span></div></div>;
|
||||
}
|
||||
|
||||
function Notice({ text }: { text: string }) { return <div className="mt-10 rounded-2xl border border-amber-200 bg-amber-50 p-8 text-sm text-amber-800">{text}</div>; }
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, ArrowRight, Search } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { PublicHeader } from "@/components/public-header";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import type { FundProduct, FundProductList } from "@/types/api";
|
||||
|
||||
export default function ProductsPage() {
|
||||
const [items, setItems] = useState<FundProduct[]>([]);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [riskLevel, setRiskLevel] = useState("");
|
||||
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams({ page: "1", page_size: "24" });
|
||||
if (keyword.trim()) params.set("keyword", keyword.trim());
|
||||
if (riskLevel) params.set("risk_level", riskLevel);
|
||||
setState("loading");
|
||||
apiFetch<FundProductList>(`/products?${params.toString()}`)
|
||||
.then((data) => { setItems(data.items); setState("ready"); })
|
||||
.catch(() => setState("error"));
|
||||
}, [keyword, riskLevel]);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-slate-50 text-slate-900">
|
||||
<PublicHeader />
|
||||
<section className="bg-[var(--brand-navy)] px-8 pb-16 pt-36 text-white"><div className="mx-auto max-w-7xl"><Link href="/" className="inline-flex items-center gap-2 text-sm text-blue-100 hover:text-white"><ArrowLeft className="h-4 w-4" />返回首页</Link><p className="mt-12 text-sm font-semibold uppercase tracking-[0.22em] text-blue-200">Fund Marketplace</p><h1 className="mt-4 text-5xl font-semibold">基金产品</h1><p className="mt-4 max-w-xl text-base leading-7 text-blue-100">基于真实产品数据,按基金类型、风险等级和投资目标了解更多选择。</p></div></section>
|
||||
<section className="mx-auto max-w-7xl px-8 py-12"><div className="flex flex-wrap gap-3"><div className="relative w-80"><Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" /><input value={keyword} onChange={(event) => setKeyword(event.target.value)} placeholder="搜索基金名称或代码" className="w-full rounded-xl border border-slate-200 bg-white py-3 pl-10 pr-4 text-sm outline-none focus:border-[var(--brand-primary)]" /></div><select value={riskLevel} onChange={(event) => setRiskLevel(event.target.value)} className="rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-700 outline-none focus:border-[var(--brand-primary)]"><option value="">全部风险等级</option><option value="低风险">低风险</option><option value="中风险">中风险</option><option value="高风险">高风险</option></select></div><div className="mt-8 grid gap-5 md:grid-cols-2 lg:grid-cols-3">{state === "loading" && [1, 2, 3, 4, 5, 6].map((item) => <div key={item} className="h-64 animate-pulse rounded-2xl bg-slate-200" />)}{state === "error" && <div className="rounded-2xl border border-amber-200 bg-amber-50 p-6 text-sm text-amber-800 lg:col-span-3">基金数据暂时无法加载,请确认后端服务已启动后重试。</div>}{state === "ready" && items.map((item) => <Link href={`/products/${item.product_code}`} key={item.id} className="group rounded-2xl border border-slate-200 bg-white p-6 transition hover:-translate-y-1 hover:border-[var(--brand-primary)] hover:shadow-xl hover:shadow-slate-200/50"><div className="flex items-start justify-between"><span className="rounded-full bg-indigo-50 px-3 py-1 text-xs font-semibold text-[var(--brand-primary)]">{item.risk_level}</span><ArrowRight className="h-4 w-4 text-slate-300 group-hover:text-[var(--brand-primary)]" /></div><h2 className="mt-8 text-lg font-semibold text-[var(--brand-navy)]">{item.product_name}</h2><p className="mt-1 text-xs text-slate-400">{item.product_code} · {item.product_type}</p><div className="mt-8 flex items-end justify-between"><div><p className="text-xs text-slate-400">最新净值</p><p className="mt-1 text-2xl font-semibold text-[var(--brand-navy)]">{item.nav ?? "--"}</p></div><span className="text-xs text-slate-400">{item.nav_date ?? "暂无日期"}</span></div></Link>)}{state === "ready" && items.length === 0 && <div className="rounded-2xl border border-slate-200 bg-white p-10 text-center text-sm text-slate-500 lg:col-span-3">没有找到匹配的基金产品。</div>}</div></section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user