礼物选择
This commit is contained in:
parent
7d6418e43c
commit
ebb4545ad6
@ -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="排序"
|
||||
|
||||
127
src/features/games/pages/RoomRpsConfigPage.test.jsx
Normal file
127
src/features/games/pages/RoomRpsConfigPage.test.jsx
Normal 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 });
|
||||
}
|
||||
77
src/features/resources/components/GiftSelectDrawer.test.jsx
Normal file
77
src/features/resources/components/GiftSelectDrawer.test.jsx
Normal 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();
|
||||
});
|
||||
@ -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)}
|
||||
/>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user