"use client"; import { Eye, PieChart, Search, Users } from "lucide-react"; import { useCallback, useEffect, useState, type ReactNode } from "react"; import { adminApi, errMessage, type Customer, type CustomerDetail, type Diagnosis, type Holding, type ReportItem, } from "@/lib/admin-api"; import { Badge, Button, Card, EmptyState, ErrorState, LoadingBlock, Modal, Pagination, SearchInput, Select, TableWrap, Td, Th, formatMoney, formatPercent, formatTime, statusTone, useToast, } from "@/components/admin/ui"; const STATUS_OPTIONS = [ { value: "", label: "全部状态" }, { value: "已分配", label: "已分配" }, { value: "已签约", label: "已签约" }, { value: "已结束", label: "已结束" }, ]; const REPORT_STATUS: Record = { draft: "草稿", sent: "已发送", discarded: "已废弃", }; export function CustomersSection() { const toast = useToast(); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [totalPages, setTotalPages] = useState(0); const [page, setPage] = useState(1); const [keyword, setKeyword] = useState(""); const [status, setStatus] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [activeId, setActiveId] = useState(null); const load = useCallback(async () => { setLoading(true); setError(""); try { const result = await adminApi.customers({ status: status || undefined, keyword: keyword || undefined, page, page_size: 10, }); setItems(result.items ?? []); setTotal(result.total ?? 0); setTotalPages(result.total_pages ?? 0); } catch (e) { setError(errMessage(e, "客户列表加载失败")); } finally { setLoading(false); } }, [status, keyword, page]); useEffect(() => { void load(); }, [load]); return ( <>
{ setKeyword(event.target.value); setPage(1); }} />
{loading ? ( ) : error ? ( void load()} /> ) : items.length === 0 ? ( ) : ( <> 客户 客户等级 风险等级 总资产 关系状态 操作 {items.map((customer) => (

{customer.real_name ?? "匿名客户"}

{customer.phone ?? "--"}

{customer.customer_level ?? "--"} {customer.risk_level ?? "--"} {customer.total_assets !== null ? formatMoney(customer.total_assets) : "--"} {customer.relation_status}
))}
)}
{activeId !== null && ( setActiveId(null)} onChanged={() => void load()} onToast={(text, tone) => toast(text, tone)} /> )} ); } function CustomerDetailModal({ customerId, onClose, onChanged, onToast, }: { customerId: number; onClose: () => void; onChanged: () => void; onToast: (text: string, tone?: "success" | "error" | "info") => void; }) { const [tab, setTab] = useState<"profile" | "holdings" | "diagnosis" | "reports">("profile"); const [detail, setDetail] = useState(null); const [holdings, setHoldings] = useState([]); const [diagnosis, setDiagnosis] = useState(null); const [reports, setReports] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [unmasked, setUnmasked] = useState(false); const [busy, setBusy] = useState(false); useEffect(() => { void (async () => { setLoading(true); setError(""); try { const [base, holdingList, diag, reportPage] = await Promise.all([ adminApi.customer(customerId), adminApi.holdings(customerId).catch(() => []), adminApi.diagnosis(customerId).catch(() => null), adminApi.reports(customerId).catch(() => null), ]); setDetail(base); setHoldings(holdingList ?? []); setDiagnosis(diag); setReports(reportPage?.items ?? []); } catch (e) { setError(errMessage(e, "客户详情加载失败")); } finally { setLoading(false); } })(); }, [customerId]); async function unmaskPhone() { setBusy(true); try { const result = await adminApi.customer(customerId, true); setDetail(result); setUnmasked(true); onToast("已查看完整手机号,操作已记入审计台账", "info"); } catch (e) { onToast(errMessage(e, "查看失败"), "error"); } finally { setBusy(false); } } async function changeRelation(action: "sign" | "close") { const text = action === "sign" ? "确认签约该客户?" : "确认结束对该客户的服务?结束後不可恢复。"; if (!window.confirm(text)) return; setBusy(true); try { await adminApi.setRelation(customerId, action); onToast(action === "sign" ? "已签约" : "已结束服务"); const refreshed = await adminApi.customer(customerId, unmasked); setDetail(refreshed); onChanged(); } catch (e) { onToast(errMessage(e, "操作失败"), "error"); } finally { setBusy(false); } } const tabs = [ { key: "profile", label: "基本资料" }, { key: "holdings", label: `持仓 (${holdings.length})` }, { key: "diagnosis", label: "组合诊断" }, { key: "reports", label: `历史报告 (${reports.length})` }, ] as const; return ( {detail?.relation_status === "已签约" ? ( ) : ( )} } > {loading ? ( ) : error ? ( ) : (
{tabs.map((item) => ( ))}
{tab === "profile" && detail && (
{detail.phone ?? "--"} {!unmasked && ( )} } /> {detail.relation_status}} />
)} {tab === "holdings" && (holdings.length === 0 ? ( ) : (
{holdings.map((holding) => ( ))}
产品 市值 浮动盈亏 收益率

{holding.product_name ?? "--"}

{holding.product_code ?? ""}

{formatMoney(holding.current_value)} = 0 ? "text-red-600" : "text-emerald-600" }`} > {formatMoney(holding.profit_loss)} = 0 ? "text-red-600" : "text-emerald-600" }`} > {formatPercent(Number(holding.profit_ratio), 2)}
))} {tab === "diagnosis" && (diagnosis ? (

持仓总市值

{formatMoney(diagnosis.total_value)}

最大单一产品占比

{formatPercent(diagnosis.concentration?.top_product_ratio ?? 0)}

{diagnosis.concentration?.top_product?.product_name ?? "--"}

资产配置分布

{Object.keys(diagnosis.allocation ?? {}).length === 0 ? ( ) : (
{Object.entries(diagnosis.allocation).map(([type, value]) => { const totalValue = Number(diagnosis.total_value) || 1; const ratio = Number(value) / totalValue; return (
{type} {formatMoney(value)} · {formatPercent(ratio)}
); })}
)}
) : ( ))} {tab === "reports" && (reports.length === 0 ? ( ) : (
    {reports.map((report) => (
  • {report.title ?? "未命名报告"}

    {report.intent ?? "--"} · {formatTime(report.send_time)}

    {REPORT_STATUS[report.send_status] ?? report.send_status}
  • ))}
))}
)} ); } function Field({ label, value }: { label: string; value: ReactNode }) { return (
{label}
{value}
); }