Files
shaguabijia-admin-web/src/components/AdjustBalanceModals.tsx
T
ouzhou a393b5c0e3 feat(config): 配置页 bool 开关控件 + 全站 antd App.useApp 迁移 (#12)
- 系统配置页支持 bool 类型(Switch 控件),供「提现自动对账」「比价期广告」等运营开关使用
- message / Modal.confirm 静态调用迁移到 App.useApp() 的 message / modal,消除 React 19
  "Static function can not consume context" 告警;providers 用 <App> 容器包裹全站

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: OuYingJun1024 <1034284404@qq.com>
Reviewed-on: #12
Co-authored-by: ouzhou <ouzhou@wonderable.ai>
Co-committed-by: ouzhou <ouzhou@wonderable.ai>
2026-06-17 10:38:40 +08:00

204 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
// 调金币 / 调现金弹窗(受控)。列表页与用户详情页共用,避免两处各写一套。
// 每个弹窗自带「增减 / 设为指定值」两种模式;打开时拉一次 360 概览展示当前余额。
import { useEffect, useState } from 'react';
import { App, Form, Input, InputNumber, Modal, Segmented } from 'antd';
import { api, errMsg } from '@/lib/api';
import { yuan } from '@/lib/format';
import type { UserOverview } from '@/lib/types';
export type AdjustTargetUser = { id: number; phone: string };
interface ModalProps {
user: AdjustTargetUser | null;
open: boolean;
onClose: () => void;
onDone?: () => void; // 调整成功后回调(刷新余额/列表)
}
const balanceLine = (balances: { coin: number; cashCents: number } | null) => (
<p style={{ color: '#888', marginBottom: 12 }}>
当前余额:金币 {balances ? balances.coin : '…'} {balances ? yuan(balances.cashCents) : '…'}
</p>
);
// 弹窗打开时拉一次该用户余额;用户切换/关闭时丢弃过期响应(alive 标记防竞态)。
function useBalances(userId: number | undefined, open: boolean) {
const [balances, setBalances] = useState<{ coin: number; cashCents: number } | null>(null);
useEffect(() => {
if (!open || userId == null) {
setBalances(null);
return;
}
let alive = true;
setBalances(null);
api
.get<UserOverview>(`/admin/api/users/${userId}`)
.then((r) => {
if (alive) setBalances({ coin: r.data.coin_balance, cashCents: r.data.cash_balance_cents });
})
.catch(() => {});
return () => {
alive = false;
};
}, [userId, open]);
return balances;
}
export function AdjustCoinModal({ user, open, onClose, onDone }: ModalProps) {
const { message } = App.useApp();
const [form] = Form.useForm();
const [mode, setMode] = useState<'delta' | 'set'>('delta');
const balances = useBalances(user?.id, open);
useEffect(() => {
if (open) {
setMode('delta');
form.resetFields();
}
}, [open, form]);
const submit = async () => {
if (!user) return;
const v = await form.validateFields();
try {
await api.post(`/admin/api/users/${user.id}/coins`, { mode, ...v });
message.success(mode === 'set' ? '已设置金币' : '已调整金币');
onClose();
onDone?.();
} catch (e) {
message.error(errMsg(e));
}
};
return (
<Modal
title={`调整金币 - ${user?.phone ?? ''}`}
open={open}
onOk={submit}
onCancel={onClose}
destroyOnHidden
>
{balanceLine(balances)}
<Form form={form} layout="vertical">
<Form.Item label="操作方式">
<Segmented
value={mode}
onChange={(v) => setMode(v as 'delta' | 'set')}
options={[
{ label: '增减', value: 'delta' },
{ label: '设为指定值', value: 'set' },
]}
/>
</Form.Item>
<Form.Item
name="amount"
label={mode === 'set' ? '目标金币值(直接设为该值)' : '金币变动(正=增加,负=扣减)'}
rules={[
{ required: true },
...(mode === 'set' ? [{ type: 'number' as const, min: 0, message: '目标值不能为负' }] : []),
]}
>
<InputNumber style={{ width: '100%' }} min={mode === 'set' ? 0 : undefined} />
</Form.Item>
<Form.Item name="reason" label="原因(入审计)" rules={[{ required: true }]}>
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
);
}
export function AdjustCashModal({ user, open, onClose, onDone }: ModalProps) {
const { message } = App.useApp();
const [form] = Form.useForm();
const [mode, setMode] = useState<'delta' | 'set'>('delta');
const balances = useBalances(user?.id, open);
useEffect(() => {
if (open) {
setMode('delta');
form.resetFields();
}
}, [open, form]);
// 输入元,×100 转分后调后端(主要给无现金用户发钱直接测试提现)
const submit = async () => {
if (!user) return;
const v = await form.validateFields();
const amountCents = Math.round(Number(v.amount_yuan) * 100);
if (mode === 'delta' && amountCents === 0) {
message.warning('金额不能为 0(且不小于 1 分)');
return;
}
if (mode === 'set' && amountCents < 0) {
message.warning('目标金额不能为负');
return;
}
try {
await api.post(`/admin/api/users/${user.id}/cash`, {
mode,
amount_cents: amountCents,
reason: v.reason,
});
message.success(
mode === 'set'
? `已设为 ¥${(amountCents / 100).toFixed(2)} 现金`
: `${amountCents > 0 ? '发放' : '扣减'} ¥${(Math.abs(amountCents) / 100).toFixed(2)} 现金`,
);
onClose();
onDone?.();
} catch (e) {
message.error(errMsg(e));
}
};
return (
<Modal
title={`调现金 - ${user?.phone ?? ''}`}
open={open}
onOk={submit}
onCancel={onClose}
destroyOnHidden
>
{balanceLine(balances)}
<Form form={form} layout="vertical">
<Form.Item label="操作方式">
<Segmented
value={mode}
onChange={(v) => setMode(v as 'delta' | 'set')}
options={[
{ label: '增减', value: 'delta' },
{ label: '设为指定值', value: 'set' },
]}
/>
</Form.Item>
<Form.Item
name="amount_yuan"
label={
mode === 'set'
? '目标现金(元,直接设为该值,须≥0)'
: '现金变动(元,正=发放,负=扣减;不可扣成负)'
}
rules={[
{ required: true },
...(mode === 'set' ? [{ type: 'number' as const, min: 0, message: '目标值不能为负' }] : []),
]}
>
<InputNumber
style={{ width: '100%' }}
step={0.01}
precision={2}
suffix="元"
min={mode === 'set' ? 0 : undefined}
/>
</Form.Item>
<Form.Item name="reason" label="原因(入审计)" rules={[{ required: true }]}>
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
);
}