"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, adminRotateCustomerKey, adminUpdateCustomer, } 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 [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 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 handleCreate = async () => { if (!token) return; const name = createName.trim(); if (!name) { setCreateError("请填写客户名称"); return; } setCreating(true); setCreateError(undefined); const res = await adminCreateCustomer("", token, { name, remark: createRemark.trim() || undefined, embed_password: createPassword.trim() || undefined, }); setCreating(false); if (res.code !== 0) { setCreateError(res.message); return; } setCreateOpen(false); setCreateName(""); setCreateRemark(""); setCreatePassword("123456"); setRevealedKey({ customer_id: res.data.customer_id, 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 totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); const activeKeyPrefix = (row: HostCustomerRecord) => row.api_keys.find((k) => k.is_active)?.key_prefix ?? "—"; return ( setCreateOpen(true)}>新建客户 } /> {revealedKey && (

客户 {revealedKey.customer_id} 的 API Key(仅显示一次,请立即复制保存)

{revealedKey.api_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 名称 状态 Key 前缀 演示密码 备注 创建时间 操作
{row.customer_id} {row.name} {row.status === "active" ? "启用" : "停用"} {activeKeyPrefix(row)}… {row.embed_password_set ? "已设置" : "未设置"} {row.remark || "—"} {formatDateTime(row.created_at)}
openPasswordDrawer(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;可设置宿主嵌入演示登录密码(默认 123456)。

setCreateName(e.target.value)} /> setCreatePassword(e.target.value)} /> setCreateRemark(e.target.value)} />
setCreateOpen(false)} > 取消 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)} > 取消 保存
)}
); }