import { useCallback, useEffect, useState } from "react"; import { Gauge, Wallet, X } from "lucide-react"; import { adminApi } from "../../api"; import { Pager } from "../../components/pager"; import { IconKitSvg } from "../../components/IconKitSvg"; import { SystemLoading } from "../../components/loading"; import type { AdminLedger, AdminQuotaPolicy, AdminTeam } from "../../types"; import { pts } from "../stage-config"; type Notify = (type: "success" | "error" | "info", text: string) => void; const PAGE_SIZE = 10; function fmtDate(iso: string) { if (!iso) return "—"; const d = new Date(iso); const p = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}/${p(d.getMonth() + 1)}/${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; } const LEDGER_TABS = [ { key: "", label: "全部" }, { key: "recharge", label: "充值" }, { key: "charge", label: "扣费" }, { key: "adjustment", label: "调额" }, { key: "refund", label: "退款" } ]; const LEDGER_LABEL: Record = { recharge: "充值", reserve: "预扣", release: "释放", charge: "扣费", adjustment: "调额", refund: "退款" }; function ledgerPill(t: string) { const cls = ["recharge", "refund", "release"].includes(t) ? "ok" : t === "adjustment" ? "info" : "neutral"; return {LEDGER_LABEL[t] || t}; } // 复用:加载团队下拉 function useTeams() { const [teams, setTeams] = useState([]); useEffect(() => { adminApi.teams({ page_size: 200 }).then((r) => setTeams(r.results)).catch(() => {}); }, []); return teams; } // ─────────────────────────── 平台计费配置 ─────────────────────────── // 积分汇率 / 视频毛利系数 / 预留 buffer(与后端计价引擎同源,改动即刻生效于下一次估价/结算) function BillingConfigCard({ notify }: { notify: Notify }) { const [form, setForm] = useState({ points_per_yuan: "", video_margin_multiplier: "", video_reserve_buffer: "" }); const [loaded, setLoaded] = useState(false); const [saving, setSaving] = useState(false); const load = useCallback(async () => { try { const cfg = await adminApi.billingConfig(); setForm({ points_per_yuan: cfg.points_per_yuan, video_margin_multiplier: cfg.video_margin_multiplier, video_reserve_buffer: cfg.video_reserve_buffer }); setLoaded(true); } catch { notify("error", "计费配置加载失败"); } }, [notify]); useEffect(() => { void load(); }, [load]); const save = async () => { setSaving(true); try { await adminApi.updateBillingConfig(form); notify("success", "计费配置已更新,即刻生效"); } catch (e) { notify("error", e instanceof Error ? e.message : "保存失败"); } finally { setSaving(false); } }; if (!loaded) return null; return (

平台计费配置

[ /billing-config ]
setForm((f) => ({ ...f, points_per_yuan: e.target.value }))} />
1 元兑换的积分数
setForm((f) => ({ ...f, video_margin_multiplier: e.target.value }))} />
用户价 = 火山成本 × 系数
setForm((f) => ({ ...f, video_reserve_buffer: e.target.value }))} />
预留 = 预估积分 × buffer
); } // ─────────────────────────── 计费审计 ─────────────────────────── export function AdminLedgersPage({ notify }: { notify: Notify }) { const [ledgers, setLedgers] = useState([]); const [count, setCount] = useState(0); const [loading, setLoading] = useState(true); const [tab, setTab] = useState(""); const [page, setPage] = useState(1); const [modalOpen, setModalOpen] = useState(false); const [form, setForm] = useState({ team: "", amount: "", reason: "" }); const [saving, setSaving] = useState(false); const teams = useTeams(); const load = useCallback(async () => { setLoading(true); try { const res = await adminApi.ledgers({ ledger_type: tab || undefined, page, page_size: PAGE_SIZE }); if (res.results.length === 0 && res.count > 0 && page > 1) { setPage((p) => Math.max(1, p - 1)); return; } setLedgers(res.results); setCount(res.count); } catch { notify("error", "加载流水失败"); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [tab, page]); useEffect(() => { void load(); }, [load]); async function doAdjust() { if (saving) return; if (!form.team) { notify("error", "请选择团队"); return; } if (!form.amount || Number(form.amount) === 0) { notify("error", "请输入非 0 金额"); return; } setSaving(true); try { await adminApi.adjustCredit({ team: form.team, amount: form.amount, reason: form.reason }); notify("success", "已调额并入账"); setModalOpen(false); setForm({ team: "", amount: "", reason: "" }); await load(); } catch (e) { notify("error", e instanceof Error ? e.message : "调额失败"); } finally { setSaving(false); } } return ( <>

计费审计

{count} 条流水 · 全局信用流水 · 手动调额
{LEDGER_TABS.map((t) => ( ))}
{loading ? ( ) : ledgers.length === 0 ? (

暂无流水

) : (
{ledgers.map((l) => ( ))}
团队用户类型金额余额后备注时间
{l.team_name || —} {l.username || —} {ledgerPill(l.ledger_type)} {pts(l.amount)} 积分 {pts(l.balance_after)} 积分 {l.reason || —} {fmtDate(l.created_at)}
)} {modalOpen && (
{ if (e.target === e.currentTarget) setModalOpen(false); }}>
++
手动调额

为团队手动加 / 减额度(争议补偿)。金额可为负;调额后余额不得为负。会落一条调额流水。

setForm((f) => ({ ...f, amount: e.target.value }))} />
setForm((f) => ({ ...f, reason: e.target.value }))} />
)} ); } // ─────────────────────────── 额度策略 ─────────────────────────── const EMPTY_POLICY = { id: "", team: "", monthly_limit: "", project_limit: "", per_task_limit: "", is_active: true }; export function AdminQuotaPage({ notify }: { notify: Notify }) { const [policies, setPolicies] = useState([]); const [count, setCount] = useState(0); const [loading, setLoading] = useState(true); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(EMPTY_POLICY); const [saving, setSaving] = useState(false); const teams = useTeams(); const [page, setPage] = useState(1); const load = useCallback(async () => { setLoading(true); try { const res = await adminApi.quotaPolicies({ page, page_size: PAGE_SIZE }); if (res.results.length === 0 && res.count > 0 && page > 1) { setPage((p) => Math.max(1, p - 1)); return; } setPolicies(res.results); setCount(res.count); } catch { if (page > 1) { setPage(1); return; } notify("error", "加载额度策略失败"); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [page]); useEffect(() => { void load(); }, [load]); function openNew() { setEditing(EMPTY_POLICY); setModalOpen(true); } function openEdit(p: AdminQuotaPolicy) { setEditing({ id: p.id, team: p.team, monthly_limit: p.monthly_limit ?? "", project_limit: p.project_limit ?? "", per_task_limit: p.per_task_limit ?? "", is_active: p.is_active }); setModalOpen(true); } async function save() { if (saving) return; if (!editing.team) { notify("error", "请选择团队"); return; } setSaving(true); const payload = { monthly_limit: editing.monthly_limit === "" ? null : editing.monthly_limit, project_limit: editing.project_limit === "" ? null : editing.project_limit, per_task_limit: editing.per_task_limit === "" ? null : editing.per_task_limit, is_active: editing.is_active }; try { if (editing.id) { await adminApi.updateQuotaPolicy(editing.id, payload); } else { await adminApi.createQuotaPolicy({ team: editing.team, ...payload }); } notify("success", "已保存额度策略"); setModalOpen(false); await load(); } catch (e) { notify("error", e instanceof Error ? e.message : "保存失败"); } finally { setSaving(false); } } async function del(p: AdminQuotaPolicy) { try { await adminApi.deleteQuotaPolicy(p.id); notify("success", "已删除策略"); await load(); } catch { notify("error", "删除失败"); } } const lim = (v: string | null) => (v == null ? 不限 : `${pts(v)} 积分`); return ( <>

额度策略

{count} 条 · 4 层额度(团队月度 / 项目 / 单任务)· 设了才拦,留空不限
{loading ? ( ) : policies.length === 0 ? (

暂无额度策略

默认仅余额管控 · 点右上新建

) : (
{policies.map((p) => ( ))}
团队月度上限项目上限单任务上限状态操作
{p.team_name || —} {lim(p.monthly_limit)} {lim(p.project_limit)} {lim(p.per_task_limit)} {p.is_active ? 启用 : 停用}
)} {modalOpen && (
{ if (e.target === e.currentTarget) setModalOpen(false); }}>
++
{editing.id ? "编辑额度策略" : "新建额度策略"}

各上限留空 = 该维度不限。任一上限触发即拦截生成(在原余额管控之上叠加)。

setEditing((p) => ({ ...p, monthly_limit: e.target.value }))} />
setEditing((p) => ({ ...p, per_task_limit: e.target.value }))} />
setEditing((p) => ({ ...p, project_limit: e.target.value }))} />
)} ); }