246 lines
10 KiB
JavaScript
246 lines
10 KiB
JavaScript
import AddOutlined from "@mui/icons-material/AddOutlined";
|
|
import Alert from "@mui/material/Alert";
|
|
import Button from "@mui/material/Button";
|
|
import Dialog from "@mui/material/Dialog";
|
|
import DialogActions from "@mui/material/DialogActions";
|
|
import DialogContent from "@mui/material/DialogContent";
|
|
import DialogTitle from "@mui/material/DialogTitle";
|
|
import MenuItem from "@mui/material/MenuItem";
|
|
import TextField from "@mui/material/TextField";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import {
|
|
buildCoinSellerRechargeExchangeRatePayload,
|
|
validateCoinSellerRechargeExchangeRatePayload,
|
|
} from "../format.js";
|
|
|
|
const EMPTY_CONFIG = { tiers: [], whitelist: [] };
|
|
|
|
export function FinanceCoinSellerExchangeRateDialog({ apps, initialAppCode, open, onClose, onLoad, onSave }) {
|
|
const rowSequence = useRef(0);
|
|
const [appCode, setAppCode] = useState("");
|
|
const [config, setConfig] = useState(EMPTY_CONFIG);
|
|
const [error, setError] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setAppCode(initialAppCode || apps[0]?.appCode || "");
|
|
setError("");
|
|
}
|
|
}, [apps, initialAppCode, open]);
|
|
|
|
useEffect(() => {
|
|
if (!open || !appCode) {
|
|
return undefined;
|
|
}
|
|
let active = true;
|
|
setLoading(true);
|
|
setError("");
|
|
onLoad(appCode)
|
|
.then((data) => {
|
|
if (!active) {
|
|
return;
|
|
}
|
|
setConfig({
|
|
tiers: (data?.tiers || []).map((item) => withRowID(item, rowSequence)),
|
|
whitelist: (data?.whitelist || []).map((item) => withRowID(item, rowSequence)),
|
|
});
|
|
})
|
|
.catch((loadError) => {
|
|
if (active) {
|
|
setConfig(EMPTY_CONFIG);
|
|
setError(loadError.message || "加载金币汇率配置失败");
|
|
}
|
|
})
|
|
.finally(() => {
|
|
if (active) {
|
|
setLoading(false);
|
|
}
|
|
});
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [appCode, onLoad, open]);
|
|
|
|
const payload = useMemo(() => buildCoinSellerRechargeExchangeRatePayload(config), [config]);
|
|
|
|
const submit = async (event) => {
|
|
event.preventDefault();
|
|
const validationMessage = validateCoinSellerRechargeExchangeRatePayload(payload);
|
|
if (validationMessage) {
|
|
setError(validationMessage);
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
setError("");
|
|
try {
|
|
await onSave(appCode, payload);
|
|
onClose();
|
|
} catch (saveError) {
|
|
setError(saveError.message || "保存金币汇率配置失败");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const updateRow = (group, id, key, value) => {
|
|
setConfig((current) => ({
|
|
...current,
|
|
[group]: current[group].map((item) => (item.id === id ? { ...item, [key]: value } : item)),
|
|
}));
|
|
setError("");
|
|
};
|
|
|
|
const addRow = (group) => {
|
|
const item =
|
|
group === "tiers"
|
|
? { coinsPerUsd: "", maxUsdAmount: "", minUsdAmount: "" }
|
|
: { coinsPerUsd: "", userId: "" };
|
|
setConfig((current) => ({ ...current, [group]: [...current[group], withRowID(item, rowSequence)] }));
|
|
setError("");
|
|
};
|
|
|
|
const removeRow = (group, id) => {
|
|
setConfig((current) => ({ ...current, [group]: current[group].filter((item) => item.id !== id) }));
|
|
setError("");
|
|
};
|
|
|
|
const disabled = loading || saving;
|
|
|
|
return (
|
|
<Dialog fullWidth maxWidth="md" open={open} onClose={disabled ? undefined : onClose}>
|
|
<DialogTitle>币商充值金币汇率</DialogTitle>
|
|
<DialogContent>
|
|
<form className="finance-rate-config" id="coin-seller-exchange-rate-form" onSubmit={submit}>
|
|
{error ? <Alert severity="error">{error}</Alert> : null}
|
|
<TextField
|
|
disabled={disabled}
|
|
label="APP"
|
|
select
|
|
value={appCode}
|
|
onChange={(event) => setAppCode(event.target.value)}
|
|
>
|
|
{apps.map((app) => (
|
|
<MenuItem key={app.appCode} value={app.appCode}>
|
|
{app.appName || app.appCode}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
|
|
<RateSection
|
|
actionLabel="添加区间"
|
|
columns={["最低 USD", "最高 USD", "1 USD 兑换金币"]}
|
|
disabled={disabled}
|
|
rows={config.tiers}
|
|
title="金额区间"
|
|
onAdd={() => addRow("tiers")}
|
|
onRemove={(id) => removeRow("tiers", id)}
|
|
renderRow={(item) => (
|
|
<>
|
|
<TextField
|
|
disabled={disabled}
|
|
slotProps={{ htmlInput: { "aria-label": "最低 USD", min: 0.01, step: "0.01" } }}
|
|
type="number"
|
|
value={item.minUsdAmount}
|
|
onChange={(event) => updateRow("tiers", item.id, "minUsdAmount", event.target.value)}
|
|
/>
|
|
<TextField
|
|
disabled={disabled}
|
|
slotProps={{ htmlInput: { "aria-label": "最高 USD", min: 0.01, step: "0.01" } }}
|
|
type="number"
|
|
value={item.maxUsdAmount}
|
|
onChange={(event) => updateRow("tiers", item.id, "maxUsdAmount", event.target.value)}
|
|
/>
|
|
<TextField
|
|
disabled={disabled}
|
|
slotProps={{ htmlInput: { "aria-label": "区间金币汇率", min: 1, step: "1" } }}
|
|
type="number"
|
|
value={item.coinsPerUsd}
|
|
onChange={(event) => updateRow("tiers", item.id, "coinsPerUsd", event.target.value)}
|
|
/>
|
|
</>
|
|
)}
|
|
/>
|
|
|
|
<RateSection
|
|
actionLabel="添加白名单"
|
|
columns={["用户 ID", "1 USD 兑换金币"]}
|
|
disabled={disabled}
|
|
rows={config.whitelist}
|
|
title="用户 ID 白名单"
|
|
onAdd={() => addRow("whitelist")}
|
|
onRemove={(id) => removeRow("whitelist", id)}
|
|
renderRow={(item) => (
|
|
<>
|
|
<TextField
|
|
disabled={disabled}
|
|
slotProps={{ htmlInput: { "aria-label": "白名单用户 ID" } }}
|
|
value={item.userId}
|
|
onChange={(event) => updateRow("whitelist", item.id, "userId", event.target.value)}
|
|
/>
|
|
<TextField
|
|
disabled={disabled}
|
|
slotProps={{ htmlInput: { "aria-label": "白名单金币汇率", min: 1, step: "1" } }}
|
|
type="number"
|
|
value={item.coinsPerUsd}
|
|
onChange={(event) => updateRow("whitelist", item.id, "coinsPerUsd", event.target.value)}
|
|
/>
|
|
</>
|
|
)}
|
|
/>
|
|
</form>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button disabled={disabled} variant="outlined" onClick={onClose}>
|
|
取消
|
|
</Button>
|
|
<Button disabled={disabled || !appCode} form="coin-seller-exchange-rate-form" type="submit" variant="contained">
|
|
保存
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function RateSection({ actionLabel, columns, disabled, onAdd, onRemove, renderRow, rows, title }) {
|
|
return (
|
|
<section className="finance-rate-config__section">
|
|
<div className="finance-rate-config__heading">
|
|
<h3>{title}</h3>
|
|
<Button disabled={disabled} size="small" startIcon={<AddOutlined fontSize="small" />} onClick={onAdd}>
|
|
{actionLabel}
|
|
</Button>
|
|
</div>
|
|
<div className="finance-rate-config__table">
|
|
<div className={`finance-rate-config__row finance-rate-config__row--head finance-rate-config__row--${columns.length}`}>
|
|
{columns.map((column) => (
|
|
<span key={column}>{column}</span>
|
|
))}
|
|
<span>操作</span>
|
|
</div>
|
|
{rows.length ? (
|
|
rows.map((item) => (
|
|
<div
|
|
className={`finance-rate-config__row finance-rate-config__row--${columns.length}`}
|
|
key={item.id}
|
|
>
|
|
{renderRow(item)}
|
|
<Button color="error" disabled={disabled} size="small" onClick={() => onRemove(item.id)}>
|
|
删除
|
|
</Button>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="finance-rate-config__empty">当前无数据</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function withRowID(item, rowSequence) {
|
|
rowSequence.current += 1;
|
|
return { ...item, id: `rate-row-${rowSequence.current}` };
|
|
}
|