a393b5c0e3
- 系统配置页支持 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>
204 lines
6.2 KiB
TypeScript
204 lines
6.2 KiB
TypeScript
'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>
|
||
);
|
||
}
|