"use client"; import Link from "next/link"; import { ArrowLeft, ArrowUpRight, ChevronRight, CircleUserRound, Landmark, RefreshCw, ShieldCheck, WalletCards, X, } from "lucide-react"; import { FormEvent, useEffect, useMemo, useState } from "react"; import { apiFetch } from "@/lib/api"; import { clearToken } from "@/lib/auth"; import type { UserPayload } from "@/types/api"; interface Balance { balance?: number | string; available_balance?: number | string; } interface Holding { product_id?: number; product_name?: string; product_code?: string; shares?: number | string; cost_amount?: number | string; current_value?: number | string; market_value?: number | string; profit_loss?: number | string; profit_ratio?: number | string; } /** /products 列表项(只取展示与跳转需要的字段)。 */ interface ProductLite { id?: number; product_code?: string; product_name?: string; product_type?: string; risk_level?: string; } interface ProductPage { items?: ProductLite[]; } /** * 持仓接口(/holdings)只返回 product_id,不含基金名称与代码, * 因此用产品列表(status=全部,含暂停/到期/清盘)在前端做一次映射, * 以便展示「具体是哪一只基金」并跳转到详情页。后端不改动。 */ async function loadProductMap(): Promise> { const page = await apiFetch("/products?status=全部&page_size=100"); const map = new Map(); for (const item of page.items ?? []) { if (typeof item.id === "number") map.set(item.id, item); } return map; } function toNumber(value: number | string | undefined | null): number | null { if (value === undefined || value === null || value === "") return null; const n = Number(value); return Number.isFinite(n) ? n : null; } function formatMoney(value: number | string | undefined | null): string { const n = toNumber(value); if (n === null) return "--"; return n.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function formatNumber(value: number | string | undefined | null, digits = 4): string { const n = toNumber(value); if (n === null) return "--"; return n.toLocaleString("zh-CN", { minimumFractionDigits: 0, maximumFractionDigits: digits }); } /** 盈亏着色:盈利红、亏损绿(国内习惯)。 */ function profitTone(value: number | string | undefined | null): string { const n = toNumber(value); if (n === null || n === 0) return "text-slate-500"; return n > 0 ? "text-rose-600" : "text-emerald-600"; } /** * /auth/me 不返回 real_name,客户账号的用户名即手机号, * 直接展示会暴露完整号码,这里做脱敏(139****0041)。 */ function maskAccountName(value: string | undefined | null): string { if (!value) return "您的华夏基金账户"; return /^\d{11}$/.test(value) ? `${value.slice(0, 3)}****${value.slice(7)}` : value; } export default function AccountPage() { const [user, setUser] = useState(null); const [balance, setBalance] = useState(null); const [holdings, setHoldings] = useState([]); const [productMap, setProductMap] = useState>(() => new Map()); const [state, setState] = useState<"loading" | "ready" | "error">("loading"); const [action, setAction] = useState<"purchase" | "redeem" | null>(null); async function loadAccount() { setState("loading"); try { const [me, account, holdingList] = await Promise.all([ apiFetch<{ user: UserPayload }>("/auth/me"), apiFetch("/account/balance"), apiFetch("/holdings"), ]); setUser(me.user); setBalance(account); setHoldings(holdingList); setState("ready"); } catch { setState("error"); } // 产品映射失败不影响账户主体数据展示,名称回退为「基金 #id」。 try { setProductMap(await loadProductMap()); } catch { /* 静默降级 */ } } useEffect(() => { void loadAccount(); }, []); function signOut() { clearToken(); window.location.href = "/login"; } /* 持仓总金额:各持仓当前市值合计。 */ const totalHoldingValue = useMemo( () => holdings.reduce( (sum, item) => sum + (toNumber(item.current_value ?? item.market_value) ?? 0), 0 ), [holdings] ); return (
华 华夏基金
基金产品
返回首页

Personal Account

用户中心

{maskAccountName(user?.real_name ?? user?.username)}

{state === "loading" && } {state === "error" && ( )} {state === "ready" && ( <>

账户余额

¥{formatMoney(balance?.balance)}

持仓总金额

¥{formatMoney(totalHoldingValue)}

共 {holdings.length} 只基金

风险测评

待完成测评

当前后端仅提供提交接口

我的持仓

{holdings.length ? (
{holdings.map((item) => { const product = item.product_id ? productMap.get(item.product_id) : undefined; const code = item.product_code ?? product?.product_code; const name = item.product_name ?? product?.product_name ?? (item.product_id ? `基金 #${item.product_id}` : "未知基金"); const meta = [code, product?.product_type, product?.risk_level] .filter(Boolean) .join(" · "); const row = ( <>

{name}

{meta || "暂无产品信息"}

¥{formatMoney(item.current_value ?? item.market_value)}

份额 {formatNumber(item.shares)} · 盈亏{" "} {formatMoney(item.profit_loss)}

{code && ( )}
); return code ? ( {row} ) : (
{row}
); })}
) : (

暂无持仓数据

)}

快捷操作

交易记录

当前后端暂未提供交易记录查询接口,页面不会展示虚构数据。

消息通知

当前后端暂未提供用户消息查询接口。

)}
{action && ( setAction(null)} onDone={() => { setAction(null); void loadAccount(); }} /> )}
); } function TradeModal({ type, holdings, onClose, onDone, }: { type: "purchase" | "redeem"; holdings: Holding[]; onClose: () => void; onDone: () => void; }) { const [productId, setProductId] = useState(String(holdings[0]?.product_id ?? "")); const [value, setValue] = useState(""); const [message, setMessage] = useState(""); const [loading, setLoading] = useState(false); async function submit(event: FormEvent) { event.preventDefault(); setLoading(true); setMessage(""); try { await apiFetch(type === "purchase" ? "/purchase" : "/redeem", { method: "POST", body: JSON.stringify( type === "purchase" ? { product_id: Number(productId), amount: value } : { product_id: Number(productId), shares: value } ), }); onDone(); } catch (error) { setMessage(error instanceof Error ? error.message : "操作失败,请稍后重试"); } finally { setLoading(false); } } return (

{type === "purchase" ? "申购基金" : "赎回基金"}

{message &&

{message}

}
); } function Notice({ text }: { text: string }) { return (
{text}
); }