可配置

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({ await updateGiftDiamondRatios({
regionId: 1001, regionId: 1001,
ratios: { lucky: "40.00", normal: "30.00", super_lucky: "50.00" }, 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]; 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({ expect(JSON.parse(String(putInit?.body))).toMatchObject({
regionId: 1001, regionId: 1001,
ratios: { lucky: "40.00", normal: "30.00", super_lucky: "50.00" }, 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; effectiveRegionId: number;
giftTypeCode: string; giftTypeCode: string;
ratioPercent: string; ratioPercent: string;
returnCoinRatioPercent: string;
regionId: number; regionId: number;
status: string; status: string;
updatedAtMs?: number; updatedAtMs?: number;
@ -226,6 +227,7 @@ export function getGiftDiamondRatios(regionId: string | number): Promise<GiftDia
export function updateGiftDiamondRatios(payload: { export function updateGiftDiamondRatios(payload: {
regionId: number; regionId: number;
ratios: Record<string, string>; ratios: Record<string, string>;
returnCoinRatios: Record<string, string>;
}): Promise<GiftDiamondRatioResponse> { }): Promise<GiftDiamondRatioResponse> {
return apiRequest<GiftDiamondRatioResponse, typeof payload>("/v1/admin/operations/gift-diamond-ratios", { return apiRequest<GiftDiamondRatioResponse, typeof payload>("/v1/admin/operations/gift-diamond-ratios", {
body: payload, body: payload,

View File

@ -235,6 +235,29 @@
gap: var(--space-4); 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 { .wheelBody {
display: grid; display: grid;
min-width: 0; min-width: 0;
@ -626,10 +649,22 @@
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.ratioGrid { .ratioGrid,
.ratioTable {
grid-template-columns: minmax(0, 1fr); 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, .wheelBody,
.wheelConfigGrid, .wheelConfigGrid,
.wheelSummaryGrid, .wheelSummaryGrid,

View File

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