"use client"; import { useCallback, useEffect, useState } from "react"; import Link from "next/link"; import { AdminLayout } from "@/components/layout/admin-layout"; import { PageHeader } from "@/components/layout/page-header"; import { Skeleton } from "@/components/ui/skeleton"; import { ErrorBanner } from "@/components/ui/error-banner"; import { SecondaryButton } from "@/components/ui/primary-button"; import { PrimaryButton } from "@/components/ui/primary-button"; import { InputField } from "@/components/ui/input-field"; import { Card } from "@/components/ui/card"; import { WarningBanner } from "@/components/ui/warning-banner"; import { adminCreateCustomer, adminGetCustomers, adminGetProviderCredentials, adminRotateCustomerKey, adminUpdateCustomer, adminUpsertProviderCredential, type ProviderCredentialProvider, type ProviderCredentialRecord, } from "@/lib/frontend/api-client"; import { formatDateTime } from "@/lib/frontend/format"; import { useAuth } from "@/hooks/use-auth"; import type { DataPageStatus, HostCustomerRecord } from "@/lib/frontend/types"; const PAGE_SIZE = 10; export default function AdminCustomersPage() { const { token } = useAuth(); const [status, setStatus] = useState("loading"); const [rows, setRows] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [keyword, setKeyword] = useState(""); const [createOpen, setCreateOpen] = useState(false); const [createName, setCreateName] = useState(""); const [createRemark, setCreateRemark] = useState(""); const [createPassword, setCreatePassword] = useState("123456"); const [createBindCreds, setCreateBindCreds] = useState(false); const [createFlockEmail, setCreateFlockEmail] = useState(""); const [createFlockPassword, setCreateFlockPassword] = useState(""); const [createMsEmail, setCreateMsEmail] = useState(""); const [createMsPassword, setCreateMsPassword] = useState(""); const [creating, setCreating] = useState(false); const [createError, setCreateError] = useState(); const [passwordTarget, setPasswordTarget] = useState( null, ); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [passwordError, setPasswordError] = useState(); const [savingPassword, setSavingPassword] = useState(false); const [revealedKey, setRevealedKey] = useState<{ customer_id: string; api_key: string; } | null>(null); const [actionError, setActionError] = useState(null); const [rotatingId, setRotatingId] = useState(null); const [credTarget, setCredTarget] = useState(null); const [credList, setCredList] = useState([]); const [credError, setCredError] = useState(); const [credLoading, setCredLoading] = useState(false); const [showCredPassword, setShowCredPassword] = useState<{ flock: boolean; mothership: boolean; }>({ flock: false, mothership: false }); const load = useCallback( async (p: number, kw: string) => { if (!token) return; setStatus("loading"); const res = await adminGetCustomers("", token, p, PAGE_SIZE, kw); if (res.code !== 0) { setStatus("error"); return; } setRows(res.data.list); setTotal(res.data.total); setStatus(res.data.total === 0 ? "empty" : "success"); }, [token], ); useEffect(() => { void load(page, keyword); }, [load, page, keyword]); const resetCreateForm = () => { setCreateName(""); setCreateRemark(""); setCreatePassword("123456"); setCreateBindCreds(false); setCreateFlockEmail(""); setCreateFlockPassword(""); setCreateMsEmail(""); setCreateMsPassword(""); setCreateError(undefined); }; const saveProviderCred = async ( customerId: string, provider: ProviderCredentialProvider, email: string, password: string, ): Promise => { if (!token) return "未登录"; const trimmedEmail = email.trim(); const trimmedPassword = password.trim(); if (!trimmedEmail && !trimmedPassword) return null; if (!trimmedEmail || !trimmedEmail.includes("@")) { return `${provider === "flock" ? "Flock Freight" : "MotherShip"}:请填写有效登录邮箱`; } if (!trimmedPassword) { return `${provider === "flock" ? "Flock Freight" : "MotherShip"}:首次配置必须填写密码`; } const res = await adminUpsertProviderCredential("", token, customerId, { provider, email: trimmedEmail, password: trimmedPassword, }); if (res.code !== 0) return res.message; return null; }; const handleCreate = async () => { if (!token) return; const name = createName.trim(); if (!name) { setCreateError("请填写客户名称"); return; } if (createBindCreds) { const hasAny = createFlockEmail.trim() || createFlockPassword.trim() || createMsEmail.trim() || createMsPassword.trim(); if (!hasAny) { setCreateError("已勾选绑定查价网站账密,请至少填写一套邮箱和密码"); return; } } setCreating(true); setCreateError(undefined); const res = await adminCreateCustomer("", token, { name, remark: createRemark.trim() || undefined, embed_password: createPassword.trim() || undefined, }); if (res.code !== 0) { setCreating(false); setCreateError(res.message); return; } const customerId = res.data.customer_id; if (createBindCreds) { const flockErr = await saveProviderCred( customerId, "flock", createFlockEmail, createFlockPassword, ); if (flockErr) { setCreating(false); setCreateError(`客户已创建,但 Flock 账密保存失败:${flockErr}`); setRevealedKey({ customer_id: customerId, api_key: res.data.api_key }); setCreateOpen(false); resetCreateForm(); void load(1, keyword); setPage(1); return; } const msErr = await saveProviderCred( customerId, "mothership", createMsEmail, createMsPassword, ); if (msErr) { setCreating(false); setCreateError(`客户已创建,但 MotherShip 账密保存失败:${msErr}`); setRevealedKey({ customer_id: customerId, api_key: res.data.api_key }); setCreateOpen(false); resetCreateForm(); void load(1, keyword); setPage(1); return; } } setCreating(false); setCreateOpen(false); resetCreateForm(); setRevealedKey({ customer_id: customerId, api_key: res.data.api_key, }); void load(1, keyword); setPage(1); }; const handleToggleStatus = async (row: HostCustomerRecord) => { if (!token) return; setActionError(null); const nextStatus = row.status === "active" ? "disabled" : "active"; const res = await adminUpdateCustomer("", token, row.customer_id, { status: nextStatus, }); if (res.code !== 0) { setActionError(res.message); return; } setRows((list) => list.map((item) => item.customer_id === row.customer_id ? res.data : item, ), ); }; const openPasswordDrawer = (row: HostCustomerRecord) => { setPasswordTarget(row); setNewPassword(""); setConfirmPassword(""); setPasswordError(undefined); }; const handleSavePassword = async () => { if (!token || !passwordTarget) return; if (newPassword.length < 6) { setPasswordError("密码至少 6 位"); return; } if (newPassword !== confirmPassword) { setPasswordError("两次输入的密码不一致"); return; } setSavingPassword(true); setPasswordError(undefined); const res = await adminUpdateCustomer("", token, passwordTarget.customer_id, { embed_password: newPassword, }); setSavingPassword(false); if (res.code !== 0) { setPasswordError(res.message); return; } setRows((list) => list.map((item) => item.customer_id === passwordTarget.customer_id ? res.data : item, ), ); setPasswordTarget(null); }; const handleRotateKey = async (customerId: string) => { if (!token) return; setRotatingId(customerId); setActionError(null); const res = await adminRotateCustomerKey("", token, customerId); setRotatingId(null); if (res.code !== 0) { setActionError(res.message); return; } setRevealedKey({ customer_id: customerId, api_key: res.data.api_key }); void load(page, keyword); }; const openCredDrawer = async (row: HostCustomerRecord) => { if (!token) return; setCredTarget(row); setCredError(undefined); setShowCredPassword({ flock: false, mothership: false }); setCredLoading(true); const res = await adminGetProviderCredentials("", token, row.customer_id); setCredLoading(false); if (res.code !== 0) { setCredError(res.message); setCredList([]); return; } setCredList(res.data.list); }; const flockCredSummary = credList.find((c) => c.provider === "flock"); const msCredSummary = credList.find((c) => c.provider === "mothership"); const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); return ( { resetCreateForm(); setCreateOpen(true); }} > 新建客户 } /> {revealedKey && (

客户 {revealedKey.customer_id} 的 API Key

{revealedKey.api_key}

已保存到客户资料,可随时查看当前有效 Key。调用方式:Authorization: {" "}Bearer <Key>;Header X-Customer-Id: {revealedKey.customer_id}

setRevealedKey(null)}> 我已保存
)} {actionError && (
{actionError}
)}
{ setPage(1); setKeyword(e.target.value); }} />
{status === "loading" && (
{Array.from({ length: 5 }).map((_, i) => ( ))}
)} {status === "error" && ( void load(page, keyword)}> 重试 } > 加载客户列表失败 )} {status === "empty" && ( 暂无客户,点击「新建客户」创建第一个租户 )} {status === "success" && (
{rows.map((row) => ( ))}
客户 ID 名称 状态 当前 API Key 演示密码 备注 创建时间 操作
{row.customer_id} {row.name} {row.status === "active" ? "启用" : "停用"} {row.active_api_key ? ( {row.active_api_key} ) : ( 历史 Key 未保存原文,请轮换一次 Key )} {row.embed_password_set ? "已设置" : "未设置"} {row.remark || "—"} {formatDateTime(row.created_at)}
openPasswordDrawer(row)} > 设置密码 void openCredDrawer(row)} > 查价网站账密 void handleToggleStatus(row)} > {row.status === "active" ? "停用" : "启用"} void handleRotateKey(row.customer_id)} > 轮换 Key 加价配置
共 {total} 条,第 {page} / {totalPages} 页
setPage((p) => Math.max(1, p - 1))} > 上一页 = totalPages} onClick={() => setPage((p) => p + 1)} > 下一页
)} {createOpen && (
!creating && setCreateOpen(false)} >
e.stopPropagation()} >

新建客户

创建后将自动生成 API Key;可选绑定 MotherShip / Flock Freight 官网账密,用于该客户查价登录拿折扣价。

setCreateName(e.target.value)} /> setCreatePassword(e.target.value)} /> setCreateRemark(e.target.value)} /> {createBindCreds && (

Flock Freight

setCreateFlockEmail(e.target.value)} placeholder="Flock 官网登录邮箱" /> setCreateFlockPassword(e.target.value)} />

MotherShip

setCreateMsEmail(e.target.value)} placeholder="MotherShip 官网登录邮箱" /> setCreateMsPassword(e.target.value)} />
)}
{ setCreateOpen(false); resetCreateForm(); }} > 取消 void handleCreate()}> 创建并生成 Key
)} {passwordTarget && (
!savingPassword && setPasswordTarget(null)} >
e.stopPropagation()} >

设置嵌入演示密码

客户 {passwordTarget.customer_id}({passwordTarget.name} )在 /embed-demo 登录时使用此密码

{ e.preventDefault(); void handleSavePassword(); }} > setNewPassword(e.target.value)} /> setConfirmPassword(e.target.value)} />
setPasswordTarget(null)} > 取消 保存
)} {credTarget && (
setCredTarget(null)} >
e.stopPropagation()} >

查价网站账密

客户 {credTarget.customer_id}({credTarget.name} )。仅供查看;账密在新建客户时写入,本页不提供修改。有账密时该客户查价强制官网登录。

{credLoading ? (

加载中…

) : (
{credError && {credError}} {( [ { key: "flock" as const, title: "Flock Freight", summary: flockCredSummary, }, { key: "mothership" as const, title: "MotherShip", summary: msCredSummary, }, ] as const ).map(({ key, title, summary }) => (

{title}

{summary?.has_password ? "已配置" : "未配置"}
{summary?.has_password ? ( <>

登录邮箱

{summary.email || "—"}

密码

{showCredPassword[key] ? summary.password || "(无法解密,请核对 JWT_SECRET)" : "••••••••"}

{summary.updated_at ? (

更新时间 {formatDateTime(summary.updated_at)}

) : null} ) : (

未绑定官网账密(新建客户时可勾选写入)

)}
))}
setCredTarget(null)} > 关闭
)}
)}
); }