import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined"; import SearchOutlined from "@mui/icons-material/SearchOutlined"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; import { useMemo, useState } from "react"; import { createCoinAdjustment, listCoinAdjustments, lookupCoinAdjustmentTarget } from "@/features/operations/api"; import { useCoinAdjustmentAbilities } from "@/features/operations/permissions.js"; import styles from "@/features/operations/operations.module.css"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; import { AdminFormAmountField, AdminFormDialog, AdminFormFieldGrid, AdminFormSection, } from "@/shared/ui/AdminFormDialog.jsx"; import { AdminActionIconButton, AdminFilterResetButton, AdminListBody, AdminListPage, AdminListToolbar, } from "@/shared/ui/AdminListLayout.jsx"; import { Button } from "@/shared/ui/Button.jsx"; import { DataState } from "@/shared/ui/DataState.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx"; import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js"; import { emptyUserIdentityFilter, isUserIdentityFilterEmpty, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js"; import { formatMillis } from "@/shared/utils/time.js"; const pageSize = 50; const adjustmentReasons = ["运营发放", "活动补发", "系统补偿", "异常扣回", "风控扣减"]; const emptyForm = () => ({ amount: "", mode: "increase", reason: adjustmentReasons[0], remark: "", userKeyword: "", }); const columns = [ { key: "user", label: "接收用户", render: (item) => , width: "minmax(260px, 1.35fr)", }, { key: "amount", label: "增减数量", render: (item) => , width: "minmax(130px, 0.65fr)", }, { key: "reason", label: "发放理由", render: (item) => item.reason || "-", width: "minmax(170px, 0.85fr)", }, { key: "remark", label: "备注", render: (item) => item.remark || "-", width: "minmax(220px, 1fr)", }, { key: "operator", label: "发放人", render: (item) => operatorName(item), width: "minmax(150px, 0.8fr)", }, { key: "balance", label: "余额", render: (item) => formatNumber(item.availableAfter), width: "minmax(120px, 0.65fr)", }, { key: "createdAtMs", label: "时间", render: (item) => formatMillis(item.createdAtMs), width: "minmax(170px, 0.85fr)", }, ]; export function CoinAdjustmentPage() { const abilities = useCoinAdjustmentAbilities(); const { showToast } = useToast(); const [page, setPage] = useState(1); const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" }); const [dialogOpen, setDialogOpen] = useState(false); const [form, setForm] = useState(emptyForm); const [targetUser, setTargetUser] = useState(null); const [lookupLoading, setLookupLoading] = useState(false); const [submitting, setSubmitting] = useState(false); const filters = useMemo( () => ({ end_at_ms: timeRange.endMs || "", start_at_ms: timeRange.startMs || "", ...userIdentityFilterParams(userFilter), }), [timeRange.endMs, timeRange.startMs, userFilter], ); const query = usePaginatedQuery({ errorMessage: "获取金币增减列表失败", fetcher: listCoinAdjustments, filters, page, pageSize, queryKey: ["coin-adjustments", filters, page], }); const data = query.data || { items: [], total: 0, page, pageSize }; const items = data.items || []; const total = data.total || 0; const tableColumns = columns.map((column) => column.key === "user" ? { ...column, filter: createUserIdentityColumnFilter({ value: userFilter, onChange: (value) => { setUserFilter(value); setPage(1); }, }), } : column, ); const resetFilters = () => { setUserFilter(emptyUserIdentityFilter); setTimeRange({ endMs: "", startMs: "" }); setPage(1); }; const changeTimeRange = (nextRange) => { setTimeRange(nextRange); setPage(1); }; const openDialog = () => { setForm(emptyForm()); setTargetUser(null); setDialogOpen(true); }; const closeDialog = () => { if (submitting) { return; } setDialogOpen(false); setTargetUser(null); }; const changeForm = (patch) => { setForm((current) => ({ ...current, ...patch })); }; const searchTargetUser = async () => { const keyword = form.userKeyword.trim(); if (!keyword) { showToast("请输入用户 ID", "error"); return; } setLookupLoading(true); try { const user = await lookupCoinAdjustmentTarget(keyword); setTargetUser(user); } catch (err) { setTargetUser(null); showToast(err.message || "查询用户失败", "error"); } finally { setLookupLoading(false); } }; const submitAdjustment = async (event) => { event.preventDefault(); if (!targetUser?.userId) { showToast("请先搜索用户", "error"); return; } let payload; try { payload = buildAdjustmentPayload(form, targetUser); } catch (err) { showToast(err.message || "金币增减参数不正确", "error"); return; } setSubmitting(true); try { await createCoinAdjustment(payload); showToast("金币已调整", "success"); setForm(emptyForm()); setTargetUser(null); setDialogOpen(false); await query.reload(); } catch (err) { showToast(err.message || "金币调整失败", "error"); } finally { setSubmitting(false); } }; return ( ) : null } filters={ <> > } /> 0 ? { page, pageSize: data.pageSize || pageSize, total, onPageChange: setPage, } : undefined } rowKey={(item) => item.entryId} /> { changeForm({ userKeyword: event.target.value }); setTargetUser(null); }} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); searchTargetUser(); } }} /> } onClick={searchTargetUser} > 搜索 {targetUser ? : null} changeForm({ remark: event.target.value })} /> changeForm({ mode: event.target.value })} > 增加 扣减 changeForm({ amount: event.target.value })} /> changeForm({ reason: event.target.value })} > {adjustmentReasons.map((reason) => ( {reason} ))} ); } function AdjustmentUserCell({ item }) { const user = item.user || {}; return ; } function TargetUserPreview({ user }) { return ; } function AdjustmentAmount({ item }) { const delta = Number(item.availableDelta ?? item.amount ?? 0); const expense = delta < 0; const sign = expense ? "-" : "+"; return ( {sign} {formatNumber(Math.abs(delta))} ); } function buildAdjustmentPayload(form, targetUser) { const rawAmount = String(form.amount || "").trim(); if (!/^[1-9]\d*$/.test(rawAmount)) { throw new Error("请输入金币数量"); } const amount = Number(rawAmount); if (!Number.isSafeInteger(amount)) { throw new Error("金币数量过大"); } const signedAmount = form.mode === "decrease" ? -amount : amount; const reason = String(form.reason || "").trim(); if (!reason) { throw new Error("请选择发放理由"); } const remark = String(form.remark || "").trim(); if (!remark) { throw new Error("请输入备注"); } if (remark.length > 512) { throw new Error("备注不能超过 512 个字符"); } return { amount: signedAmount, commandId: `coin-adjustment-${Date.now()}`, reason, remark, targetUserId: String(targetUser.userId), }; } function operatorName(item) { const operator = item.operator || {}; return operator.name || operator.username || item.operatorUserId || "-"; } function formatNumber(value) { return Number(value || 0).toLocaleString("zh-CN"); }