@@ -3,7 +3,7 @@
// 调金币 / 调现金弹窗(受控)。列表页与用户详情页共用,避免两处各写一套。
// 每个弹窗自带「增减 / 设为指定值」两种模式;打开时拉一次 360 概览展示当前余额。
import { useEffect , useState } from 'react' ;
import { App , Form , Input , InputNumber , Modal , Segmented } from 'antd' ;
import { App , Form , Input , InputNumber , Modal , Radio , Segmented } from 'antd' ;
import { api , errMsg } from '@/lib/api' ;
import { yuan } from '@/lib/format' ;
import type { UserOverview } from '@/lib/types' ;
@@ -17,15 +17,19 @@ interface ModalProps {
onDone ? : ( ) = > void ; // 调整成功后回调(刷新余额/列表)
}
const balanceLine = ( balances : { coin : number ; cashCents : number } | null ) = > (
type Balances = { coin : number ; cashCents : number ; inviteCashCents : number } ;
const balanceLine = ( balances : Balances | null ) = > (
< p style = { { color : '#888' , marginBottom : 12 } } >
当前余额 :金币 { balances ? balances . coin : '…' } | 现 金 { balances ? yuan ( balances . cashCents ) : '… ' }
当前余额 :金币 { balances ? balances . coin : '…' } | 现 金 ( 金 币 兑 现 金 ) { ' ' }
{ balances ? yuan ( balances . cashCents ) : '…' } | 邀 请 奖 励 金 { ' ' }
{ balances ? yuan ( balances . inviteCashCents ) : '…' }
< / p >
) ;
// 弹窗打开时拉一次该用户余额;用户切换/关闭时丢弃过期响应(alive 标记防竞态)。
function useBalances ( userId : number | undefined , open : boolean ) {
const [ balances , setBalances ] = useState < { coin : number ; cashCents : number } | null > ( null ) ;
const [ balances , setBalances ] = useState < Balances | null > ( null ) ;
useEffect ( ( ) = > {
if ( ! open || userId == null ) {
setBalances ( null ) ;
@@ -36,7 +40,12 @@ function useBalances(userId: number | undefined, open: boolean) {
api
. get < UserOverview > ( ` /admin/api/users/ ${ userId } ` )
. then ( ( r ) = > {
if ( alive ) setBalances ( { coin : r.data.coin_balance , cashCents : r.data.cash_balance_cents } ) ;
if ( alive )
setBalances ( {
coin : r.data.coin_balance ,
cashCents : r.data.cash_balance_cents ,
inviteCashCents : r.data.invite_cash_balance_cents ,
} ) ;
} )
. catch ( ( ) = > { } ) ;
return ( ) = > {
@@ -113,39 +122,39 @@ export function AdjustCoinModal({ user, open, onClose, onDone }: ModalProps) {
export function AdjustCashModal ( { user , open , onClose , onDone } : ModalProps ) {
const { message } = App . useApp ( ) ;
const [ form ] = Form . useForm ( ) ;
const [ mode , setMode ] = useState < 'delta' | 'set' > ( 'delta' ) ;
// 操作方式改为表单必选项(无默认、手动选);用 watch 驱动金额栏文案/最小值
const mode = Form . useWatch ( 'mode' , form ) as 'delta' | 'set' | undefined ;
const balances = useBalances ( user ? . id , open ) ;
useEffect ( ( ) = > {
if ( open ) {
setMode ( 'delta' ) ;
form . resetFields ( ) ;
}
if ( open ) form . resetFields ( ) ; // 目标账户 / 操作方式 都重置为「未选」
} , [ open , form ] ) ;
// 输入元,×100 转分后调后端(主要给无现金用户发钱直接测试提现)
const submit = async ( ) = > {
if ( ! user ) return ;
const v = await form . validateFields ( ) ;
const v = await form . validateFields ( ) ; // 任一必填/必选缺失 → 该项下方红字 + 阻断提交
const amountCents = Math . round ( Number ( v . amount_yuan ) * 100 ) ;
if ( mode === 'delta' && amountCents === 0 ) {
if ( v . mode === 'delta' && amountCents === 0 ) {
message . warning ( '金额不能为 0(且不小于 1 分)' ) ;
return ;
}
if ( mode === 'set' && amountCents < 0 ) {
if ( v . mode === 'set' && amountCents < 0 ) {
message . warning ( '目标金额不能为负' ) ;
return ;
}
const acctName = v . account === 'invite_cash' ? '邀请奖励金' : '现金' ;
try {
await api . post ( ` /admin/api/users/ ${ user . id } /cash ` , {
mode ,
account : v.account ,
mode : v.mode ,
amount_cents : amountCents ,
reason : v.reason ,
} ) ;
message . success (
mode === 'set'
? ` 已设为 ¥ ${ ( amountCents / 100 ) . toFixed ( 2 ) } 现金 `
: ` 已 ${ amountCents > 0 ? '发放' : '扣减' } ¥ ${ ( Math . abs ( amountCents ) / 100 ) . toFixed ( 2 ) } 现金 ` ,
v . mode === 'set'
? ` 已设为 ¥ ${ ( amountCents / 100 ) . toFixed ( 2 ) } ${ acctName } `
: ` 已 ${ amountCents > 0 ? '发放' : '扣减' } ¥ ${ ( Math . abs ( amountCents ) / 100 ) . toFixed ( 2 ) } ${ acctName } ` ,
) ;
onClose ( ) ;
onDone ? . ( ) ;
@@ -164,10 +173,20 @@ export function AdjustCashModal({ user, open, onClose, onDone }: ModalProps) {
>
{ balanceLine ( balances ) }
< Form form = { form } layout = "vertical" >
< Form.Item label = "操作方式" >
< Segmented
value = { mode }
onChange = { ( v ) = > setMode ( v as 'delta' | 'set' ) }
< Form.Item name = "account" label = "目标账户" rules = { [ { required : true , message : '请选择目标账户' } ] } >
< Radio.Group
optionType = "button"
buttonStyle = "solid"
options = { [
{ label : '金币兑现金账户' , value : 'coin_cash' } ,
{ label : '邀请账户' , value : 'invite_cash' } ,
] }
/ >
< / Form.Item >
< Form.Item name = "mode" label = "操作方式" rules = { [ { required : true , message : '请选择操作方式' } ] } >
< Radio.Group
optionType = "button"
buttonStyle = "solid"
options = { [
{ label : '增减' , value : 'delta' } ,
{ label : '设为指定值' , value : 'set' } ,
@@ -182,7 +201,7 @@ export function AdjustCashModal({ user, open, onClose, onDone }: ModalProps) {
: '现金变动(元,正=发放,负=扣减;不可扣成负)'
}
rules = { [
{ required : true } ,
{ required : true , message : '请输入现金变动金额' } ,
. . . ( mode === 'set' ? [ { type : 'number' as const , min : 0 , message : '目标值不能为负' } ] : [ ] ) ,
] }
>
@@ -194,7 +213,7 @@ export function AdjustCashModal({ user, open, onClose, onDone }: ModalProps) {
min = { mode === 'set' ? 0 : undefined }
/ >
< / Form.Item >
< Form.Item name = "reason" label = "原因(入审计)" rules = { [ { required : true } ] } >
< Form.Item name = "reason" label = "原因(入审计)" rules = { [ { required : true , message : '请填写原因' } ] } >
< Input.TextArea rows = { 2 } / >
< / Form.Item >
< / Form >