可配置
This commit is contained in:
parent
0789e02c6a
commit
57231bc95a
@ -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" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user