import { adminApi, ApiError } from "../api/client"; import type { AdminRole, LedgerEntry, PageResult, UserDetail, UserSummary, UserUsageAggregate, } from "../api/types"; import { renderEmpty, renderError, renderLoading, setButtonBusy, showToast, } from "../components/ui"; import { createIdempotencyKey, escapeHtml, formatDateTime, formatNumber, formatSignedCredits, statusLabel, } from "../lib/format"; export function renderUsers(container: HTMLElement, role: AdminRole): void { container.innerHTML = `
账户管理

用户查询

按内部用户 ID 精确查询,不展示 Apple 身份标识。

搜索
${renderEmpty("输入查询条件开始搜索")}
`; const form = container.querySelector("[data-search-form]"); form?.addEventListener("submit", (event) => { event.preventDefault(); const search = new FormData(form).get("query")?.toString().trim() ?? ""; if (search) void searchUsers(container, search, role); }); } async function searchUsers( container: HTMLElement, search: string, role: AdminRole, ): Promise { const results = container.querySelector("[data-results]"); if (!results) return; renderLoading(results, "搜索用户"); try { const page = await adminApi.users(search); if (page.items.length === 0) { results.innerHTML = renderEmpty("未找到匹配用户"); return; } results.innerHTML = `
${page.items.map(userRow).join("")}
用户查询结果
用户状态积分余额注册时间操作
`; results.querySelectorAll("[data-user-id]").forEach( (button) => { button.addEventListener("click", () => { const userId = button.dataset.userId; if (userId) void renderUserDetail(container, userId, role); }); }, ); } catch (error) { renderError(results, error, () => void searchUsers(container, search, role)); } } function userRow(user: UserSummary): string { return ` ${escapeHtml(user.displayName || "未命名用户")}
${escapeHtml(user.userId)}
${statusLabel(user.status)} ${formatNumber(user.creditBalance)} ${formatDateTime(user.createdAt)} 查看 `; } async function renderUserDetail( container: HTMLElement, userId: string, role: AdminRole, ): Promise { renderLoading(container, "加载用户详情"); try { const [user, ledger] = await Promise.all([ adminApi.user(userId), adminApi.ledger(userId), ]); container.innerHTML = detailTemplate(user, ledger, role); container .querySelector("[data-back]") ?.addEventListener("click", () => { renderUsers(container, role); container.querySelector('input[name="query"]')?.focus(); }); bindLedgerPagination(container, user, ledger.nextCursor); if (role === "SUPER_ADMIN") bindGrantDialog(container, user, role); const heading = container.querySelector("h1"); if (heading) { heading.tabIndex = -1; heading.focus({ preventScroll: true }); } } catch (error) { renderError(container, error, () => void renderUserDetail(container, userId, role), ); } } function detailTemplate( user: UserDetail, ledger: PageResult, role: AdminRole, ): string { const usage = user.usage ?? []; const usageRequests = usage.reduce((total, item) => total + item.requests, 0); const chargedCredits = usage.reduce( (total, item) => total + item.chargedCredits, 0, ); const referral = user.referral; const inviterUserId = referral?.inviterUserId ?? user.referredByUserId; const qualifiedUsage = user.qualifiedUsage || usageRequests > 0; return `
← 返回用户查询
用户详情

${escapeHtml(user.displayName || "未命名用户")}

${escapeHtml(user.userId)}

${ role === "SUPER_ADMIN" ? '人工赠送积分' : "" }
${escapeHtml(user.displayName || "未命名用户")}${statusLabel(user.status)}
积分余额
${formatNumber(user.creditBalance)}
注册时间
${formatDateTime(user.createdAt)}
最近活跃
${formatDateTime(user.lastActiveAt)}
有效使用
${qualifiedUsage ? "已达成" : "未达成"}${usage.length > 0 ? ` · ${formatNumber(usageRequests)} 次请求` : ""}
${ usage.length > 0 ? `
累计消耗
${formatNumber(chargedCredits)} 积分
` : "" }
邀请码
${escapeHtml(user.referralCode || "—")}
邀请来源
${escapeHtml(inviterUserId || "—")}
${ referral ? `
邀请成效
${formatNumber(referral.rewardedInvites)} / ${formatNumber(referral.invitedUsers)} 已奖励
` : "" }

积分流水

不可变账本记录

${ ledger.items.length === 0 ? renderEmpty("暂无积分流水") : `
${ledgerRows(ledger.items)}
${escapeHtml(user.displayName || user.userId)} 的积分流水
时间类型变动结余原因
${ ledger.nextCursor ? '
加载更多流水
' : "" }

` }
${usagePanel(usage)}
${role === "SUPER_ADMIN" ? grantDialog(user) : ""} `; } function ledgerRows(entries: LedgerEntry[]): string { return entries .map( (entry) => ` ${formatDateTime(entry.createdAt)} ${escapeHtml(statusLabel(entry.type))} ${formatSignedCredits(entry.amount)} ${formatNumber(entry.balanceAfter)} ${escapeHtml(entry.reasonCode)} `, ) .join(""); } function usagePanel(usage: UserUsageAggregate[]): string { if (usage.length === 0) return ""; return `

使用统计

按使用类型汇总,不包含用户内容

${usage .map( (item) => ` `, ) .join("")}
用户使用统计
类型请求消耗积分语音时长(毫秒)输入 Token输出 Token
${escapeHtml(item.kind)} ${formatNumber(item.requests)} ${formatNumber(item.chargedCredits)} ${formatNumber(item.asrMillis)} ${formatNumber(item.inputTokens)} ${formatNumber(item.outputTokens)}
`; } function bindLedgerPagination( container: HTMLElement, user: UserDetail, initialCursor?: string, ): void { const button = container.querySelector("[data-ledger-more]"); const body = container.querySelector("[data-ledger-body]"); const status = container.querySelector("[data-ledger-status]"); if (!button || !body || !initialCursor) return; let cursor: string | undefined = initialCursor; button.addEventListener("click", async () => { if (!cursor) return; setButtonBusy(button, true, "加载中…"); try { const page = await adminApi.ledger(user.userId, cursor); body.insertAdjacentHTML("beforeend", ledgerRows(page.items)); cursor = page.nextCursor; if (status) { status.textContent = cursor ? `已加载 ${formatNumber(page.items.length)} 条更多流水` : `已加载 ${formatNumber(page.items.length)} 条流水,全部记录已加载`; } if (!cursor) { button.closest(".pagination-actions")?.remove(); } else { setButtonBusy(button, false); button.focus(); } } catch (error) { showToast( error instanceof ApiError ? error.message : "加载积分流水失败", "error", ); setButtonBusy(button, false); button.focus(); } }); } function grantDialog(user: UserDetail): string { return `
高风险操作

人工赠送积分

赠送将写入不可变账本,并记录管理员审计日志。

`; } function bindGrantDialog( container: HTMLElement, user: UserDetail, role: AdminRole, ): void { const dialog = container.querySelector("[data-grant-dialog]"); const form = container.querySelector("[data-grant-form]"); const inputs = container.querySelector("[data-grant-inputs]"); const confirm = container.querySelector("[data-grant-confirm]"); if (!dialog || !form || !inputs || !confirm) return; let idempotencyKey = createIdempotencyKey(); let outcomeUnknown = false; container .querySelector("[data-open-grant]") ?.addEventListener("click", () => { idempotencyKey = createIdempotencyKey(); outcomeUnknown = false; form.reset(); inputs.hidden = false; confirm.hidden = true; form.querySelectorAll(".form-error").forEach((node) => { node.textContent = ""; }); dialog.showModal(); form.querySelector('input[name="amount"]')?.focus(); }); dialog.addEventListener("cancel", (event) => { if (!outcomeUnknown) return; event.preventDefault(); const errorNode = form.querySelector("[data-confirm-error]"); if (errorNode) { errorNode.textContent = "赠送结果尚未确认,请使用当前窗口安全重试"; } }); container .querySelector("[data-review-grant]") ?.addEventListener("click", () => { const data = new FormData(form); const amount = Number(data.get("amount")); const reason = data.get("reason")?.toString().trim() ?? ""; const errorNode = form.querySelector("[data-error]"); if (!Number.isSafeInteger(amount) || amount < 1 || amount > 100_000) { if (errorNode) errorNode.textContent = "积分必须是 1 至 100,000 的整数"; return; } if (reason.length < 4) { if (errorNode) errorNode.textContent = "请填写至少 4 个字符的赠送原因"; return; } if (errorNode) errorNode.textContent = ""; const amountNode = form.querySelector("[data-confirm-amount]"); const reasonNode = form.querySelector("[data-confirm-reason]"); if (amountNode) amountNode.textContent = formatNumber(amount); if (reasonNode) reasonNode.textContent = `原因:${reason}`; inputs.hidden = true; confirm.hidden = false; form.querySelector("[data-confirm-title]")?.focus(); }); container .querySelector("[data-edit-grant]") ?.addEventListener("click", () => { if (outcomeUnknown) { const errorNode = form.querySelector("[data-confirm-error]"); if (errorNode) { errorNode.textContent = "结果尚未确认,不能修改本次赠送内容;请直接重试"; } return; } confirm.hidden = true; inputs.hidden = false; form.querySelector('input[name="amount"]')?.focus(); }); const submit = container.querySelector("[data-submit-grant]"); submit?.addEventListener("click", async () => { const data = new FormData(form); const amount = Number(data.get("amount")); const reason = data.get("reason")?.toString().trim() ?? ""; const errorNode = form.querySelector("[data-confirm-error]"); setButtonBusy(submit, true, "赠送中…"); if (errorNode) errorNode.textContent = ""; try { await adminApi.grantCredits({ userId: user.userId, amount, reason, idempotencyKey, }); dialog.close(); showToast("积分赠送成功,账本已更新", "success"); await renderUserDetail(container, user.userId, role); } catch (error) { const message = error instanceof ApiError ? error.message : "赠送失败,请重试"; outcomeUnknown = !(error instanceof ApiError) || error.status === 0 || error.status >= 500; if (errorNode) { errorNode.textContent = outcomeUnknown ? "赠送结果尚未确认;重试会复用同一请求,不会重复到账" : message; } setButtonBusy(submit, false); } }); }