diff --git a/src/features/operations/api.test.ts b/src/features/operations/api.test.ts index 202b36e..a9e629c 100644 --- a/src/features/operations/api.test.ts +++ b/src/features/operations/api.test.ts @@ -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" }, }); }); diff --git a/src/features/operations/api.ts b/src/features/operations/api.ts index ca7ed84..275d524 100644 --- a/src/features/operations/api.ts +++ b/src/features/operations/api.ts @@ -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; + returnCoinRatios: Record; }): Promise { return apiRequest("/v1/admin/operations/gift-diamond-ratios", { body: payload, diff --git a/src/features/operations/operations.module.css b/src/features/operations/operations.module.css index 7b4b836..2146bb0 100644 --- a/src/features/operations/operations.module.css +++ b/src/features/operations/operations.module.css @@ -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, diff --git a/src/features/operations/pages/GiftDiamondRatioPage.jsx b/src/features/operations/pages/GiftDiamondRatioPage.jsx index 9463e95..81332e4 100644 --- a/src/features/operations/pages/GiftDiamondRatioPage.jsx +++ b/src/features/operations/pages/GiftDiamondRatioPage.jsx @@ -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() {

{selectedRegionLabel}

-
+
+
礼物类型
+
主播钻石比例
+
返还金币比例
{giftTypes.map((giftType) => ( - %, - }} - disabled={!abilities.canUpdate || saving} - onChange={(event) => changeRatio(giftType.value, event.target.value)} - /> +
+
{giftType.label}
+ %, + }} + disabled={!abilities.canUpdate || saving} + onChange={(event) => changeRatio("ratios", giftType.value, event.target.value)} + /> + %, + }} + disabled={!abilities.canUpdate || saving} + onChange={(event) => + changeRatio("returnCoinRatios", giftType.value, event.target.value) + } + /> +
))}