礼物选择

This commit is contained in:
zhx 2026-06-11 17:20:54 +08:00
parent 7d6418e43c
commit ebb4545ad6
4 changed files with 240 additions and 8 deletions

View File

@ -6,6 +6,8 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { PERMISSIONS } from "@/app/permissions";
import { useAuth } from "@/app/auth/AuthProvider.jsx";
import { getRoomRpsConfig, updateRoomRpsConfig } from "@/features/games/api";
import { listGifts } from "@/features/resources/api";
import { GiftSelectField } from "@/features/resources/components/GiftSelectDrawer.jsx";
import styles from "@/features/games/games.module.css";
import {
AdminActionIconButton,
@ -47,6 +49,8 @@ export function RoomRpsConfigPage() {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState(false);
const [gifts, setGifts] = useState([]);
const [giftsLoading, setGiftsLoading] = useState(true);
const load = useCallback(async () => {
setLoading(true);
@ -60,9 +64,27 @@ export function RoomRpsConfigPage() {
}
}, [showToast]);
const loadGifts = useCallback(async () => {
setGiftsLoading(true);
try {
const result = await listGifts({ page_size: 200, status: "active" });
setGifts(result.items || []);
} catch (err) {
showToast(err.message || "加载礼物列表失败", "error");
} finally {
setGiftsLoading(false);
}
}, [showToast]);
useEffect(() => {
load();
}, [load]);
loadGifts();
}, [load, loadGifts]);
const refresh = useCallback(() => {
load();
loadGifts();
}, [load, loadGifts]);
const rows = useMemo(() => (config ? [config] : []), [config]);
const columns = useMemo(
@ -158,7 +180,7 @@ export function RoomRpsConfigPage() {
<AdminListPage>
<AdminListToolbar
actions={
<AdminActionIconButton label="刷新" onClick={load}>
<AdminActionIconButton label="刷新" onClick={refresh}>
<RefreshOutlined fontSize="small" />
</AdminActionIconButton>
}
@ -173,6 +195,8 @@ export function RoomRpsConfigPage() {
</AdminListBody>
<RoomRpsConfigDialog
form={form}
gifts={gifts}
giftsLoading={giftsLoading}
loading={saving}
open={editing}
setForm={setForm}
@ -183,7 +207,7 @@ export function RoomRpsConfigPage() {
);
}
function RoomRpsConfigDialog({ form, loading, open, setForm, onClose, onSubmit }) {
function RoomRpsConfigDialog({ form, gifts, giftsLoading, loading, open, setForm, onClose, onSubmit }) {
return (
<AdminFormDialog
loading={loading}
@ -222,11 +246,14 @@ function RoomRpsConfigDialog({ form, loading, open, setForm, onClose, onSubmit }
<div className={styles.giftTierList}>
{form.stakeGifts.map((gift, index) => (
<div className={styles.giftTierRow} key={gift.sortOrder || index}>
<TextField
label={`${index + 1} 档礼物 ID`}
type="number"
<GiftSelectField
drawerTitle={`选择第 ${index + 1} 档礼物`}
gifts={gifts}
label={`${index + 1} 档礼物`}
loading={giftsLoading}
placeholder="点击选择礼物"
value={gift.giftId}
onChange={(event) => setGiftTier(setForm, form, index, { giftId: event.target.value })}
onChange={(value) => setGiftTier(setForm, form, index, { giftId: value })}
/>
<TextField
label="排序"

View File

@ -0,0 +1,127 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { AppProviders } from "@/app/providers.jsx";
import { RoomRpsConfigPage } from "./RoomRpsConfigPage.jsx";
vi.mock("@/app/auth/AuthProvider.jsx", () => ({
AuthProvider: ({ children }) => children,
useAuth: () => ({ can: () => true }),
}));
vi.mock("@/shared/ui/SideDrawer.jsx", () => ({
SideDrawer({ actions, children, drawerProps, onClose, open, title }) {
if (!open) {
return null;
}
return (
<aside aria-label={title} data-z-index={drawerProps?.sx?.zIndex} role="dialog">
<button type="button" onClick={onClose}>
关闭
</button>
{children}
{actions}
</aside>
);
},
}));
const patchBodies = [];
beforeEach(() => {
patchBodies.length = 0;
vi.stubGlobal("fetch", vi.fn(mockFetch));
});
afterEach(() => {
vi.unstubAllGlobals();
});
test("room rps gift tier uses shared gift drawer selection before saving", async () => {
const user = userEvent.setup();
render(
<AppProviders>
<MemoryRouter>
<RoomRpsConfigPage />
</MemoryRouter>
</AppProviders>,
);
expect(await screen.findByText("房内猜拳")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "编辑" }));
expect(await screen.findByRole("dialog", { name: "房内猜拳配置" })).toBeInTheDocument();
await user.click(screen.getByDisplayValue("Rock Rose"));
expect(screen.getByRole("dialog", { name: "选择第 1 档礼物" })).toHaveAttribute("data-z-index", "1600");
await user.click(screen.getByText("Lucky Star"));
expect(screen.getByDisplayValue("Lucky Star")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "保存配置" }));
await waitFor(() => expect(patchBodies).toHaveLength(1));
expect(patchBodies[0].stakeGifts[0]).toMatchObject({
enabled: true,
giftId: 10005,
sortOrder: 1,
});
});
async function mockFetch(input, init = {}) {
const url = String(input);
const method = init.method || "GET";
if (url.includes("/v1/admin/game/room-rps/config") && method === "GET") {
return jsonResponse({
config: {
challengeTimeoutMs: 600000,
gameId: "room_rps",
revealCountdownMs: 3000,
stakeGifts: [
{ enabled: true, giftId: "10001", giftName: "Rock Rose", sortOrder: 1 },
{ enabled: true, giftId: "10002", giftName: "Paper Bell", sortOrder: 2 },
{ enabled: true, giftId: "10003", giftName: "Scissor Crown", sortOrder: 3 },
{ enabled: true, giftId: "10004", giftName: "Victory Cup", sortOrder: 4 },
],
status: "active",
},
});
}
if (url.includes("/v1/admin/gifts")) {
return jsonResponse({
items: [
{ coinPrice: 10, giftId: "10001", giftTypeCode: "normal", name: "Rock Rose", resourceId: 11 },
{ coinPrice: 20, giftId: "10005", giftTypeCode: "normal", name: "Lucky Star", resourceId: 12 },
],
page: 1,
pageSize: 200,
total: 2,
});
}
if (url.includes("/v1/admin/game/room-rps/config") && method === "PATCH") {
const body = JSON.parse(String(init.body || "{}"));
patchBodies.push(body);
return jsonResponse({
config: {
...body,
gameId: "room_rps",
stakeGifts: body.stakeGifts.map((gift) => ({
...gift,
giftId: String(gift.giftId),
})),
},
});
}
return jsonResponse(null);
}
function jsonResponse(data) {
return new Response(JSON.stringify({ code: 0, data }), { status: 200 });
}

View File

@ -0,0 +1,77 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { useState } from "react";
import { expect, test, vi } from "vitest";
import { GiftSelectField } from "./GiftSelectDrawer.jsx";
vi.mock("@/shared/ui/SideDrawer.jsx", () => ({
SideDrawer({ children, drawerProps, onClose, open, title }) {
if (!open) {
return null;
}
return (
<aside aria-label={title} data-z-index={drawerProps?.sx?.zIndex} role="dialog">
<button type="button" onClick={onClose}>
关闭
</button>
{children}
</aside>
);
},
}));
const gifts = [
{
chargeAssetType: "COIN",
coinPrice: 10,
giftId: "10001",
giftTypeCode: "normal",
name: "Rose Gift",
resource: { previewUrl: "https://media.haiyihy.com/rose.png", resourceCode: "rose_resource" },
resourceId: 11,
},
{
chargeAssetType: "COIN",
coinPrice: 20,
giftId: "10002",
giftTypeCode: "normal",
name: "Lucky Star",
resource: { resourceCode: "lucky_star_resource" },
resourceId: 12,
},
];
test("gift select field opens right drawer and writes selected gift id", () => {
const changes = [];
function Harness() {
const [value, setValue] = useState("");
return (
<GiftSelectField
gifts={gifts}
value={value}
onChange={(nextValue, gift) => {
changes.push({ gift, value: nextValue });
setValue(nextValue);
}}
/>
);
}
render(<Harness />);
fireEvent.click(screen.getByPlaceholderText("请选择礼物"));
expect(screen.getByRole("dialog", { name: "选择礼物" })).toHaveAttribute("data-z-index", "1600");
expect(screen.getByText("Rose Gift")).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText("搜索礼物名称、礼物 ID、资源编码"), {
target: { value: "star" },
});
expect(screen.queryByText("Rose Gift")).not.toBeInTheDocument();
fireEvent.click(screen.getByText("Lucky Star"));
expect(changes.at(-1)).toMatchObject({ value: "10002", gift: { giftId: "10002", name: "Lucky Star" } });
expect(screen.getByDisplayValue("Lucky Star")).toBeInTheDocument();
expect(screen.queryByRole("dialog", { name: "选择礼物" })).not.toBeInTheDocument();
});

View File

@ -195,6 +195,7 @@ export function GiftListPage() {
disabled={!page.abilities.canDeleteGift || items.length === 0 || deletingBatch}
indeterminate={someChecked}
size="small"
slotProps={{ input: { "aria-label": "选择当前页礼物" } }}
onChange={(event) => toggleAllGifts(event.target.checked)}
/>
),
@ -206,8 +207,8 @@ export function GiftListPage() {
<Checkbox
checked={selectedGiftIdSet.has(giftId)}
disabled={!page.abilities.canDeleteGift || deletingBatch}
inputProps={{ "aria-label": `选择 ${gift.name || gift.giftId}` }}
size="small"
slotProps={{ input: { "aria-label": `选择 ${gift.name || gift.giftId}` } }}
onChange={(event) => toggleGiftSelection(gift, event.target.checked)}
/>
);