From 59901a3cabbc493b0dd54d96071c971a6ab4a274 Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 14 Jul 2026 18:48:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=A4=9A=E6=AC=A1=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FinanceCoinSellerGrantDialog.jsx | 100 ++++++++++++++++++ .../FinanceCoinSellerRechargeOrderList.jsx | 23 ++-- ...inanceCoinSellerRechargeOrderList.test.jsx | 62 +++++++++-- finance/src/styles/index.css | 25 +++++ 4 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 finance/src/components/FinanceCoinSellerGrantDialog.jsx diff --git a/finance/src/components/FinanceCoinSellerGrantDialog.jsx b/finance/src/components/FinanceCoinSellerGrantDialog.jsx new file mode 100644 index 0000000..ba2b812 --- /dev/null +++ b/finance/src/components/FinanceCoinSellerGrantDialog.jsx @@ -0,0 +1,100 @@ +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogTitle from "@mui/material/DialogTitle"; +import TextField from "@mui/material/TextField"; +import { useEffect, useState } from "react"; + +export function FinanceCoinSellerGrantDialog({ loading, onClose, onGrant, order }) { + const [confirmationInput, setConfirmationInput] = useState(""); + const [finalConfirmOpen, setFinalConfirmOpen] = useState(false); + const confirmationText = buildCoinSellerGrantConfirmationText(order); + const inputMatches = confirmationInput === confirmationText; + + useEffect(() => { + // 每次切换订单都必须清空复输内容和二次确认状态,避免上一笔订单的验证结果被复用到新订单。 + setConfirmationInput(""); + setFinalConfirmOpen(false); + }, [order?.id]); + + const close = () => { + if (!loading) { + onClose(); + } + }; + + const openFinalConfirm = () => { + // 按钮禁用是主要约束,这里仍重复校验,防止脚本触发或状态切换绕过完整文字验证。 + if (inputMatches) { + setFinalConfirmOpen(true); + } + }; + + const grant = () => { + // 接口调用前再次绑定当前订单和完整校验文本,确保只有三重验证流程的最后一步能够发放金币。 + if (order && inputMatches) { + onGrant(order); + // 提交后立即关闭,避免接口状态回写前重复点击;失败信息仍由页面现有 toast 统一反馈。 + onClose(); + } + }; + + return ( + <> + + 发放金币验证 + + {confirmationText} + 请在下方输入框完整填写一遍上面的文字,中间不能有空格 + setConfirmationInput(event.target.value)} + /> + + + + + + + + + 确认发放金币 + + 是否确认发放? + {confirmationText} + + + + + + + + ); +} + +export function buildCoinSellerGrantConfirmationText(order) { + const usdAmount = plainNumber(order?.usdAmount ?? Number(order?.usdMinorAmount || 0) / 100); + const userId = order?.targetDisplayUserId || order?.targetUserId || "-"; + const coinAmount = plainNumber(order?.coinAmount); + return `本订单金额${usdAmount}USDT将会向用户${userId}的币商账户发放${coinAmount}金币`; +} + +function plainNumber(value) { + const number = Number(value); + return Number.isFinite(number) ? String(number) : "-"; +} diff --git a/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx b/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx index df87fc2..38fe601 100644 --- a/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx +++ b/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx @@ -32,6 +32,7 @@ import { } from "../format.js"; import { FinanceCoinSellerRechargeCreateDialog } from "./FinanceCoinSellerRechargeCreateDialog.jsx"; import { FinanceCoinSellerExchangeRateDialog } from "./FinanceCoinSellerExchangeRateDialog.jsx"; +import { FinanceCoinSellerGrantDialog } from "./FinanceCoinSellerGrantDialog.jsx"; export function FinanceCoinSellerRechargeOrderList({ actionLoading, @@ -63,6 +64,7 @@ export function FinanceCoinSellerRechargeOrderList({ const [rateConfigOpen, setRateConfigOpen] = useState(false); const [receiptVerification, setReceiptVerification] = useState(null); const [receiptLoading, setReceiptLoading] = useState(false); + const [grantTarget, setGrantTarget] = useState(null); const items = orders.items || []; const page = Number(orders.page || filters.page || 1); const pageSize = Number(orders.pageSize || 50); @@ -212,16 +214,13 @@ export function FinanceCoinSellerRechargeOrderList({ } }; - const confirmGrant = (order) => { - const userText = order.targetDisplayUserId || order.targetUserId || "-"; - const ok = window.confirm( - isZeroCoinMakeupOrder(order) - ? `确认完成用户 ${userText} 的补单?该订单计入充值,不发放金币。` - : `确认给用户 ${userText} 发放 ${formatAmount(order.coinAmount)} 金币?`, - ); - if (ok) { + const requestGrant = (order) => { + if (isZeroCoinMakeupOrder(order)) { + // 0 金币订单只完成补单状态且不会触发钱包发币,因此按业务要求跳过金币发放确认流程。 onGrant(order); + return; } + setGrantTarget(order); }; return ( @@ -426,7 +425,7 @@ export function FinanceCoinSellerRechargeOrderList({ size="small" startIcon={} variant="contained" - onClick={() => confirmGrant(item)} + onClick={() => requestGrant(item)} > {isZeroCoinMakeupOrder(item) ? "完成补单" : "发放"} @@ -496,6 +495,12 @@ export function FinanceCoinSellerRechargeOrderList({ onLoad={onLoadExchangeRate} onSave={onSaveExchangeRate} /> + setGrantTarget(null)} + onGrant={onGrant} + /> ); } diff --git a/finance/src/components/FinanceCoinSellerRechargeOrderList.test.jsx b/finance/src/components/FinanceCoinSellerRechargeOrderList.test.jsx index 424d8b6..da709f3 100644 --- a/finance/src/components/FinanceCoinSellerRechargeOrderList.test.jsx +++ b/finance/src/components/FinanceCoinSellerRechargeOrderList.test.jsx @@ -84,7 +84,7 @@ test("coin seller recharge order create dialog verifies receipt before create", usdAmount: 10, }), ); -}); +}, 10000); test("makeup order keeps zero coins while creating a normal recharge record", async () => { const nowMS = new Date(2026, 6, 13, 9, 0, 0, 0).getTime(); @@ -136,10 +136,6 @@ test("makeup order keeps zero coins while creating a normal recharge record", as test("coin seller recharge order actions follow verify and grant status", () => { const onVerify = vi.fn(); const onGrant = vi.fn(); - vi.stubGlobal( - "confirm", - vi.fn(() => true), - ); render( expect(actionHeader).toHaveClass("finance-flat-table__actions-cell"); expect(actionCell).toHaveClass("finance-flat-table__actions-cell"); expect(onVerify).toHaveBeenCalledWith(expect.objectContaining({ id: "pending" })); - expect(onGrant).toHaveBeenCalledWith(expect.objectContaining({ id: "verified" })); + expect(screen.getByRole("dialog", { name: "发放金币验证" })).toBeInTheDocument(); + expect(onGrant).not.toHaveBeenCalled(); expect(screen.getAllByText("人工补单")[0]).toBeInTheDocument(); expect(grantButtons[0]).toBeDisabled(); expect(verifyButtons[1]).toBeDisabled(); }); -test("zero coin seller recharge order uses makeup completion action", () => { +test("coin grant requires exact text and a final confirmation before calling the API", () => { + const onGrant = vi.fn(); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "发放" })); + + const statement = "本订单金额1USDT将会向用户7777的币商账户发放80000金币"; + const input = screen.getByRole("textbox", { name: "完整输入发放文字" }); + const firstConfirm = screen.getByRole("button", { name: "确认" }); + expect(screen.getByText(statement)).toBeInTheDocument(); + expect(firstConfirm).toBeDisabled(); + + fireEvent.change(input, { target: { value: `${statement} ` } }); + expect(firstConfirm).toBeDisabled(); + fireEvent.change(input, { target: { value: statement } }); + expect(firstConfirm).not.toBeDisabled(); + fireEvent.click(firstConfirm); + + expect(screen.getByRole("dialog", { name: "确认发放金币" })).toBeInTheDocument(); + expect(onGrant).not.toHaveBeenCalled(); + fireEvent.click(screen.getByRole("button", { name: "确认发放" })); + expect(onGrant).toHaveBeenCalledTimes(1); + expect(onGrant).toHaveBeenCalledWith(expect.objectContaining({ id: "7777", coinAmount: 80000 })); +}); + +test("zero coin seller recharge order completes makeup without confirmation", () => { const onGrant = vi.fn(); - const confirm = vi.fn(() => true); - vi.stubGlobal("confirm", confirm); render( { expect(screen.getByText("USD 10.00")).toHaveClass("finance-recharge-amount"); expect(screen.getByText("0 金币(补单)")).not.toHaveClass("finance-recharge-coins--positive"); - expect(confirm).toHaveBeenCalledWith(expect.stringContaining("不发放金币")); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(onGrant).toHaveBeenCalledWith(expect.objectContaining({ id: "makeup", coinAmount: 0 })); }); diff --git a/finance/src/styles/index.css b/finance/src/styles/index.css index 74b86fd..55ee2af 100644 --- a/finance/src/styles/index.css +++ b/finance/src/styles/index.css @@ -536,6 +536,31 @@ a { font-weight: 600; } +.finance-grant-dialog { + display: grid; + gap: var(--space-4); + padding-top: var(--space-2) !important; +} + +.finance-grant-dialog__statement { + color: var(--text-primary); + font-size: 16px; + line-height: 1.7; + overflow-wrap: anywhere; +} + +.finance-grant-dialog__warning { + color: var(--danger); + font-size: 13px; + font-weight: 700; +} + +.finance-grant-dialog--final span { + color: var(--text-secondary); + line-height: 1.65; + overflow-wrap: anywhere; +} + .finance-create-dialog { padding: var(--space-2) var(--space-5) var(--space-5) !important; }