调现金弹窗:目标账户选择 + 四项必填校验 + 邀请余额展示
- 新增「目标账户」单选(金币兑现金账户 / 邀请账户),默认不选、必选 - 操作方式改为必选无默认(原默认「增减」),进来需手动选 - 四项(目标账户/操作方式/现金变动/原因)全部必填必选:标题带红星 *, 点确定时缺项 → 字段下方红字报错并阻断提交 - 提交携带 account;弹窗顶部余额行加显「邀请奖励金」(UserOverview 新字段) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
// 调金币 / 调现金弹窗(受控)。列表页与用户详情页共用,避免两处各写一套。
|
// 调金币 / 调现金弹窗(受控)。列表页与用户详情页共用,避免两处各写一套。
|
||||||
// 每个弹窗自带「增减 / 设为指定值」两种模式;打开时拉一次 360 概览展示当前余额。
|
// 每个弹窗自带「增减 / 设为指定值」两种模式;打开时拉一次 360 概览展示当前余额。
|
||||||
import { useEffect, useState } from 'react';
|
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 { api, errMsg } from '@/lib/api';
|
||||||
import { yuan } from '@/lib/format';
|
import { yuan } from '@/lib/format';
|
||||||
import type { UserOverview } from '@/lib/types';
|
import type { UserOverview } from '@/lib/types';
|
||||||
@@ -17,15 +17,19 @@ interface ModalProps {
|
|||||||
onDone?: () => void; // 调整成功后回调(刷新余额/列表)
|
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 }}>
|
<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>
|
</p>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 弹窗打开时拉一次该用户余额;用户切换/关闭时丢弃过期响应(alive 标记防竞态)。
|
// 弹窗打开时拉一次该用户余额;用户切换/关闭时丢弃过期响应(alive 标记防竞态)。
|
||||||
function useBalances(userId: number | undefined, open: boolean) {
|
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(() => {
|
useEffect(() => {
|
||||||
if (!open || userId == null) {
|
if (!open || userId == null) {
|
||||||
setBalances(null);
|
setBalances(null);
|
||||||
@@ -36,7 +40,12 @@ function useBalances(userId: number | undefined, open: boolean) {
|
|||||||
api
|
api
|
||||||
.get<UserOverview>(`/admin/api/users/${userId}`)
|
.get<UserOverview>(`/admin/api/users/${userId}`)
|
||||||
.then((r) => {
|
.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(() => {});
|
.catch(() => {});
|
||||||
return () => {
|
return () => {
|
||||||
@@ -113,39 +122,39 @@ export function AdjustCoinModal({ user, open, onClose, onDone }: ModalProps) {
|
|||||||
export function AdjustCashModal({ user, open, onClose, onDone }: ModalProps) {
|
export function AdjustCashModal({ user, open, onClose, onDone }: ModalProps) {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [form] = Form.useForm();
|
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);
|
const balances = useBalances(user?.id, open);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) form.resetFields(); // 目标账户 / 操作方式 都重置为「未选」
|
||||||
setMode('delta');
|
|
||||||
form.resetFields();
|
|
||||||
}
|
|
||||||
}, [open, form]);
|
}, [open, form]);
|
||||||
|
|
||||||
// 输入元,×100 转分后调后端(主要给无现金用户发钱直接测试提现)
|
// 输入元,×100 转分后调后端(主要给无现金用户发钱直接测试提现)
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
const v = await form.validateFields();
|
const v = await form.validateFields(); // 任一必填/必选缺失 → 该项下方红字 + 阻断提交
|
||||||
const amountCents = Math.round(Number(v.amount_yuan) * 100);
|
const amountCents = Math.round(Number(v.amount_yuan) * 100);
|
||||||
if (mode === 'delta' && amountCents === 0) {
|
if (v.mode === 'delta' && amountCents === 0) {
|
||||||
message.warning('金额不能为 0(且不小于 1 分)');
|
message.warning('金额不能为 0(且不小于 1 分)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mode === 'set' && amountCents < 0) {
|
if (v.mode === 'set' && amountCents < 0) {
|
||||||
message.warning('目标金额不能为负');
|
message.warning('目标金额不能为负');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const acctName = v.account === 'invite_cash' ? '邀请奖励金' : '现金';
|
||||||
try {
|
try {
|
||||||
await api.post(`/admin/api/users/${user.id}/cash`, {
|
await api.post(`/admin/api/users/${user.id}/cash`, {
|
||||||
mode,
|
account: v.account,
|
||||||
|
mode: v.mode,
|
||||||
amount_cents: amountCents,
|
amount_cents: amountCents,
|
||||||
reason: v.reason,
|
reason: v.reason,
|
||||||
});
|
});
|
||||||
message.success(
|
message.success(
|
||||||
mode === 'set'
|
v.mode === 'set'
|
||||||
? `已设为 ¥${(amountCents / 100).toFixed(2)} 现金`
|
? `已设为 ¥${(amountCents / 100).toFixed(2)} ${acctName}`
|
||||||
: `已${amountCents > 0 ? '发放' : '扣减'} ¥${(Math.abs(amountCents) / 100).toFixed(2)} 现金`,
|
: `已${amountCents > 0 ? '发放' : '扣减'} ¥${(Math.abs(amountCents) / 100).toFixed(2)} ${acctName}`,
|
||||||
);
|
);
|
||||||
onClose();
|
onClose();
|
||||||
onDone?.();
|
onDone?.();
|
||||||
@@ -164,10 +173,20 @@ export function AdjustCashModal({ user, open, onClose, onDone }: ModalProps) {
|
|||||||
>
|
>
|
||||||
{balanceLine(balances)}
|
{balanceLine(balances)}
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
<Form.Item label="操作方式">
|
<Form.Item name="account" label="目标账户" rules={[{ required: true, message: '请选择目标账户' }]}>
|
||||||
<Segmented
|
<Radio.Group
|
||||||
value={mode}
|
optionType="button"
|
||||||
onChange={(v) => setMode(v as 'delta' | 'set')}
|
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={[
|
options={[
|
||||||
{ label: '增减', value: 'delta' },
|
{ label: '增减', value: 'delta' },
|
||||||
{ label: '设为指定值', value: 'set' },
|
{ label: '设为指定值', value: 'set' },
|
||||||
@@ -182,7 +201,7 @@ export function AdjustCashModal({ user, open, onClose, onDone }: ModalProps) {
|
|||||||
: '现金变动(元,正=发放,负=扣减;不可扣成负)'
|
: '现金变动(元,正=发放,负=扣减;不可扣成负)'
|
||||||
}
|
}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true },
|
{ required: true, message: '请输入现金变动金额' },
|
||||||
...(mode === 'set' ? [{ type: 'number' as const, min: 0, 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}
|
min={mode === 'set' ? 0 : undefined}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="reason" label="原因(入审计)" rules={[{ required: true }]}>
|
<Form.Item name="reason" label="原因(入审计)" rules={[{ required: true, message: '请填写原因' }]}>
|
||||||
<Input.TextArea rows={2} />
|
<Input.TextArea rows={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export interface UserOverview {
|
|||||||
user: UserListItem;
|
user: UserListItem;
|
||||||
coin_balance: number;
|
coin_balance: number;
|
||||||
cash_balance_cents: number;
|
cash_balance_cents: number;
|
||||||
|
invite_cash_balance_cents: number; // 邀请奖励金余额(与 cash_balance_cents 物理隔离)
|
||||||
total_coin_earned: number;
|
total_coin_earned: number;
|
||||||
comparison_total: number;
|
comparison_total: number;
|
||||||
comparison_success: number;
|
comparison_success: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user