From 4fa183badd3400d42d352ca22cfac04bd551907e Mon Sep 17 00:00:00 2001 From: cattie <2237829695@qq.com> Date: Fri, 31 Jul 2026 10:40:50 +0800 Subject: [PATCH] fix: merchant distribution alert and confirm window --- .../common/merchant/merchant-distribute.tsx | 140 +++++++++++++++--- .../lib/services/merchant/merchant.service.ts | 11 +- 2 files changed, 123 insertions(+), 28 deletions(-) diff --git a/frontend/components/common/merchant/merchant-distribute.tsx b/frontend/components/common/merchant/merchant-distribute.tsx index 568c561d..94fb9422 100644 --- a/frontend/components/common/merchant/merchant-distribute.tsx +++ b/frontend/components/common/merchant/merchant-distribute.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { useState } from "react" +import { useEffect, useState } from "react" import { toast } from "sonner" import { Coins } from "lucide-react" import { Button } from "@/components/ui/button" @@ -19,7 +19,18 @@ import { Label } from "@/components/ui/label" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Spinner } from "@/components/ui/spinner" -import { MerchantService, type MerchantAPIKey } from "@/lib/services" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { ConfigService, MerchantService, type MerchantAPIKey, type UserPayConfig } from "@/lib/services" +import { useUser } from "@/contexts/user-context" interface DistributeDialogProps { /** 自定义触发器 */ @@ -34,12 +45,38 @@ interface DistributeDialogProps { */ export function DistributeDialog({ trigger, apiKey }: DistributeDialogProps) { const [open, setOpen] = useState(false) + const [confirmOpen, setConfirmOpen] = useState(false) const [loading, setLoading] = useState(false) + const [userPayConfigs, setUserPayConfigs] = useState([]) const [userId, setUserId] = useState("") const [username, setUsername] = useState("") const [amount, setAmount] = useState("") const [remark, setRemark] = useState("") + const { user } = useUser() + + useEffect(() => { + const loadUserPayConfigs = async () => { + try { + setUserPayConfigs(await ConfigService.getUserPayConfigs()) + } catch { + // The distribution request remains authoritative if the rate preview cannot load. + } + } + + loadUserPayConfigs() + }, []) + + const distributeRate = userPayConfigs.find( + (config) => user?.pay_level !== undefined && config.level === user.pay_level, + )?.distribute_rate + const distributeRatePercent = distributeRate === undefined + ? null + : Number(distributeRate) * 100 + const distributionAmount = Number(amount) + const recipientAmount = Number.isFinite(distributionAmount) && distributeRatePercent !== null + ? distributionAmount - Math.round(distributionAmount * (distributeRatePercent / 100) * 100) / 100 + : null const resetForm = () => { setUserId("") @@ -52,41 +89,78 @@ export function DistributeDialog({ trigger, apiKey }: DistributeDialogProps) { if (newOpen && !loading) { resetForm() } + if (!newOpen) { + setConfirmOpen(false) + } setOpen(newOpen) } - const handleSubmit = async () => { + const validateForm = (): string | null => { if (!userId.trim()) { - toast.error('表单验证失败', { description: '请填写用户 ID' }) - return + return '请填写用户 ID' } if (!username.trim()) { - toast.error('表单验证失败', { description: '请填写用户名' }) - return + return '请填写用户名' } if (!amount.trim()) { - toast.error('表单验证失败', { description: '请填写分发金额' }) - return + return '请填写分发金额' } - const amountNum = parseFloat(amount) - if (isNaN(amountNum) || amountNum <= 0) { - toast.error('表单验证失败', { description: '分发金额必须大于 0' }) - return + if (!Number.isFinite(distributionAmount) || distributionAmount <= 0) { + return '分发金额必须大于 0' } - if (amountNum > 999999.99) { - toast.error('表单验证失败', { description: '分发金额不能超过 999999.99' }) - return + if (distributionAmount > 999999.99) { + return '分发金额不能超过 999999.99' } if (remark.length > 100) { - toast.error('表单验证失败', { description: '备注不能超过 100 个字符' }) + return '备注不能超过 100 个字符' + } + + return null + } + + const handleDistributeClick = () => { + const validationError = validateForm() + if (validationError) { + toast.error('表单验证失败', { description: validationError }) return } + setConfirmOpen(true) + } + + const handleServiceError = (error: unknown) => { + console.error('积分分发失败:', error) + + const errorMessage = error instanceof Error ? error.message : '' + const errorMap: Record = { + '收款人不存在': '用户 ID 与用户名不匹配或用户不存在', + '不能转账给自己': '不能向自己的账户分发积分', + '余额不足': '可用积分不足,请减少分发金额后重试', + '已超过每日限额': '今日分发额度已达上限,请明日再试', + '商户信息不存在': '商户账户不可用,请检查应用凭证或联系管理员', + '支付配置不存在': '当前账户的分发配置不可用,请联系管理员', + '金额必须大于0': '分发金额必须大于 0', + '金额小数位数不能超过2位': '分发金额最多保留两位小数', + '认证': '应用凭证无效或已失效,请重新创建应用凭证', + '无法连接': '无法连接到分发服务,请稍后重试', + '请求超时': '分发请求超时,请稍后重试', + '请求频率': '操作过于频繁,请稍后重试', + '权限不足': '当前应用没有分发权限,请检查应用凭证', + '服务器内部错误': '分发服务暂时不可用,请稍后重试', + } + const userMessage = Object.entries(errorMap).find(([key]) => + errorMessage.includes(key), + )?.[1] || '分发失败,请检查填写内容后重试' + + toast.error('分发失败', { description: userMessage }) + } + + const handleSubmit = async () => { try { setLoading(true) @@ -95,10 +169,10 @@ export function DistributeDialog({ trigger, apiKey }: DistributeDialogProps) { return } - const result = await MerchantService.distribute({ + const response = await MerchantService.distribute({ user_id: Number(userId.trim()), username: username.trim(), - amount: amountNum, + amount: distributionAmount, remark: remark.trim() || undefined, }, { client_id: apiKey.client_id, @@ -106,14 +180,13 @@ export function DistributeDialog({ trigger, apiKey }: DistributeDialogProps) { }) toast.success('分发成功', { - description: `订单号: ${ result.trade_no }` + description: `订单号: ${response.data.trade_no}` }) setOpen(false) resetForm() } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : '分发失败' - toast.error('分发失败', { description: errorMessage }) + handleServiceError(error) } finally { setLoading(false) } @@ -138,6 +211,10 @@ export function DistributeDialog({ trigger, apiKey }: DistributeDialogProps) {
+
+ 分发费率: {distributeRatePercent === null ? '加载中' : `${distributeRatePercent.toFixed(2)}%`} +
+