可配置

This commit is contained in:
zhx 2026-06-22 20:59:41 +08:00
parent 0789e02c6a
commit 57231bc95a
4 changed files with 111 additions and 33 deletions

View File

@ -30,6 +30,7 @@ test("gift diamond ratio APIs use operations paths", async () => {
await updateGiftDiamondRatios({
regionId: 1001,
ratios: { lucky: "40.00", normal: "30.00", super_lucky: "50.00" },
returnCoinRatios: { lucky: "10.00", normal: "30.00", super_lucky: "1.00" },
});
const [getUrl, getInit] = vi.mocked(fetch).mock.calls[0];
@ -43,6 +44,7 @@ test("gift diamond ratio APIs use operations paths", async () => {
expect(JSON.parse(String(putInit?.body))).toMatchObject({
regionId: 1001,
ratios: { lucky: "40.00", normal: "30.00", super_lucky: "50.00" },
returnCoinRatios: { lucky: "10.00", normal: "30.00", super_lucky: "1.00" },
});
});

View File

@ -16,6 +16,7 @@ export interface GiftDiamondRatioItem {
effectiveRegionId: number;
giftTypeCode: string;
ratioPercent: string;
returnCoinRatioPercent: string;
regionId: number;
status: string;
updatedAtMs?: number;
@ -226,6 +227,7 @@ export function getGiftDiamondRatios(regionId: string | number): Promise<GiftDia
export function updateGiftDiamondRatios(payload: {
regionId: number;
ratios: Record<string, string>;
returnCoinRatios: Record<string, string>;
}): Promise<GiftDiamondRatioResponse> {
return apiRequest<GiftDiamondRatioResponse, typeof payload>("/v1/admin/operations/gift-diamond-ratios", {
body: payload,

View File

@ -235,6 +235,29 @@
gap: var(--space-4);
}
.ratioTable {
display: grid;
grid-template-columns: minmax(160px, 0.8fr) repeat(2, minmax(180px, 1fr));
align-items: center;
gap: var(--space-3);
}
.ratioTableHeader {
color: var(--text-tertiary);
font-size: 12px;
font-weight: 800;
}
.ratioRow {
display: contents;
}
.ratioType {
min-width: 0;
color: var(--text-primary);
font-weight: 760;
}
.wheelBody {
display: grid;
min-width: 0;
@ -626,10 +649,22 @@
}
@media (max-width: 720px) {
.ratioGrid {
.ratioGrid,
.ratioTable {
grid-template-columns: minmax(0, 1fr);
}
.ratioTableHeader {
display: none;
}
.ratioRow {
display: grid;
gap: var(--space-3);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--border-soft);
}
.wheelBody,
.wheelConfigGrid,
.wheelSummaryGrid,

View File

@ -21,8 +21,14 @@ const giftTypes = [
const defaultRatios = {
normal: "100.00",
lucky: "100.00",
super_lucky: "100.00",
lucky: "10.00",
super_lucky: "1.00",
};
const defaultReturnCoinRatios = {
normal: "30.00",
lucky: "10.00",
super_lucky: "1.00",
};
export function GiftDiamondRatioPage() {
@ -30,7 +36,7 @@ export function GiftDiamondRatioPage() {
const { showToast } = useToast();
const { loadingRegions, regionOptions } = useRegionOptions();
const [regionId, setRegionId] = useState("0");
const [form, setForm] = useState(defaultRatios);
const [form, setForm] = useState({ ratios: defaultRatios, returnCoinRatios: defaultReturnCoinRatios });
const [saving, setSaving] = useState(false);
const queryFn = useCallback(() => getGiftDiamondRatios(regionId), [regionId]);
@ -40,13 +46,16 @@ export function GiftDiamondRatioPage() {
});
useEffect(() => {
const next = { ...defaultRatios };
const nextRatios = { ...defaultRatios };
const nextReturnCoinRatios = { ...defaultReturnCoinRatios };
for (const item of query.data?.items || []) {
if (Object.prototype.hasOwnProperty.call(next, item.giftTypeCode)) {
next[item.giftTypeCode] = item.ratioPercent || "100.00";
if (Object.prototype.hasOwnProperty.call(nextRatios, item.giftTypeCode)) {
nextRatios[item.giftTypeCode] = item.ratioPercent || defaultRatios[item.giftTypeCode];
nextReturnCoinRatios[item.giftTypeCode] =
item.returnCoinRatioPercent || defaultReturnCoinRatios[item.giftTypeCode];
}
}
setForm(next);
setForm({ ratios: nextRatios, returnCoinRatios: nextReturnCoinRatios });
}, [query.data]);
const selectedRegionLabel = useMemo(() => {
@ -56,31 +65,42 @@ export function GiftDiamondRatioPage() {
return regionOptions.find((option) => option.value === regionId)?.label || `区域 ${regionId}`;
}, [regionId, regionOptions]);
const changeRatio = (giftType, value) => {
setForm((current) => ({ ...current, [giftType]: value }));
const changeRatio = (field, giftType, value) => {
setForm((current) => ({ ...current, [field]: { ...current[field], [giftType]: value } }));
};
const submit = async () => {
const payload = {};
const ratios = {};
const returnCoinRatios = {};
for (const giftType of giftTypes) {
const value = String(form[giftType.value] || "").trim();
const value = String(form.ratios[giftType.value] || "").trim();
const numeric = Number(value);
if (!value || !Number.isFinite(numeric) || numeric < 0 || numeric > 100) {
showToast(`${giftType.label}比例必须在 0-100 之间`, "error");
showToast(`${giftType.label}主播钻石比例必须在 0-100 之间`, "error");
return;
}
payload[giftType.value] = numeric.toFixed(2);
ratios[giftType.value] = numeric.toFixed(2);
const returnCoinValue = String(form.returnCoinRatios[giftType.value] || "").trim();
const returnCoinNumeric = Number(returnCoinValue);
if (!returnCoinValue || !Number.isFinite(returnCoinNumeric) || returnCoinNumeric < 0 || returnCoinNumeric > 100) {
showToast(`${giftType.label}返还金币比例必须在 0-100 之间`, "error");
return;
}
returnCoinRatios[giftType.value] = returnCoinNumeric.toFixed(2);
}
setSaving(true);
try {
const result = await updateGiftDiamondRatios({ regionId: Number(regionId), ratios: payload });
const next = { ...defaultRatios };
const result = await updateGiftDiamondRatios({ regionId: Number(regionId), ratios, returnCoinRatios });
const nextRatios = { ...defaultRatios };
const nextReturnCoinRatios = { ...defaultReturnCoinRatios };
for (const item of result.items || []) {
if (Object.prototype.hasOwnProperty.call(next, item.giftTypeCode)) {
next[item.giftTypeCode] = item.ratioPercent || "100.00";
if (Object.prototype.hasOwnProperty.call(nextRatios, item.giftTypeCode)) {
nextRatios[item.giftTypeCode] = item.ratioPercent || defaultRatios[item.giftTypeCode];
nextReturnCoinRatios[item.giftTypeCode] =
item.returnCoinRatioPercent || defaultReturnCoinRatios[item.giftTypeCode];
}
}
setForm(next);
setForm({ ratios: nextRatios, returnCoinRatios: nextReturnCoinRatios });
showToast("礼物钻石比例已保存", "success");
await query.reload();
} catch (err) {
@ -124,21 +144,40 @@ export function GiftDiamondRatioPage() {
<div className={styles.ratioHeader}>
<h2>{selectedRegionLabel}</h2>
</div>
<div className={styles.ratioGrid}>
<div className={styles.ratioTable}>
<div className={styles.ratioTableHeader}>礼物类型</div>
<div className={styles.ratioTableHeader}>主播钻石比例</div>
<div className={styles.ratioTableHeader}>返还金币比例</div>
{giftTypes.map((giftType) => (
<TextField
key={giftType.value}
label={giftType.label}
type="number"
size="small"
value={form[giftType.value] || ""}
inputProps={{ max: 100, min: 0, step: "0.01" }}
InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment>,
}}
disabled={!abilities.canUpdate || saving}
onChange={(event) => changeRatio(giftType.value, event.target.value)}
/>
<div key={giftType.value} className={styles.ratioRow}>
<div className={styles.ratioType}>{giftType.label}</div>
<TextField
label="主播钻石比例"
type="number"
size="small"
value={form.ratios[giftType.value] || ""}
inputProps={{ max: 100, min: 0, step: "0.01" }}
InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment>,
}}
disabled={!abilities.canUpdate || saving}
onChange={(event) => changeRatio("ratios", giftType.value, event.target.value)}
/>
<TextField
label="返还金币比例"
type="number"
size="small"
value={form.returnCoinRatios[giftType.value] || ""}
inputProps={{ max: 100, min: 0, step: "0.01" }}
InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment>,
}}
disabled={!abilities.canUpdate || saving}
onChange={(event) =>
changeRatio("returnCoinRatios", giftType.value, event.target.value)
}
/>
</div>
))}
</div>
</section>