游戏用户白名单
This commit is contained in:
parent
0299d57aa4
commit
e021c51500
@ -2406,6 +2406,100 @@
|
||||
"x-permissions": ["game:status"]
|
||||
}
|
||||
},
|
||||
"/admin/game/games/{game_id}/whitelist": {
|
||||
"patch": {
|
||||
"operationId": "setGameWhitelistEnabled",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "game_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "game:update",
|
||||
"x-permissions": ["game:update"]
|
||||
}
|
||||
},
|
||||
"/admin/game/games/{game_id}/whitelist/users": {
|
||||
"get": {
|
||||
"operationId": "listGameWhitelistUsers",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "game_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "game:view",
|
||||
"x-permissions": ["game:view"]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "addGameWhitelistUser",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "game_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "game:update",
|
||||
"x-permissions": ["game:update"]
|
||||
}
|
||||
},
|
||||
"/admin/game/games/{game_id}/whitelist/users/{user_id}": {
|
||||
"delete": {
|
||||
"operationId": "deleteGameWhitelistUser",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "game_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "user_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "game:update",
|
||||
"x-permissions": ["game:update"]
|
||||
}
|
||||
},
|
||||
"/admin/game/platforms": {
|
||||
"get": {
|
||||
"operationId": "listPlatforms",
|
||||
|
||||
@ -40,6 +40,17 @@ export interface GameCatalogDto {
|
||||
tags: string[];
|
||||
createdAtMs?: number;
|
||||
updatedAtMs?: number;
|
||||
whitelistEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface GameWhitelistUserDto {
|
||||
userId: string;
|
||||
displayUserId?: string;
|
||||
username?: string;
|
||||
avatar?: string;
|
||||
status?: string;
|
||||
createdByAdminId?: number;
|
||||
createdAtMs?: number;
|
||||
}
|
||||
|
||||
export interface GameCatalogPage {
|
||||
@ -254,7 +265,7 @@ export type GamePlatformPayload = Omit<
|
||||
GamePlatformDto,
|
||||
"appCode" | "createdAtMs" | "updatedAtMs" | "callbackSecretSet"
|
||||
>;
|
||||
export type GameCatalogPayload = Omit<GameCatalogDto, "appCode" | "createdAtMs" | "updatedAtMs">;
|
||||
export type GameCatalogPayload = Omit<GameCatalogDto, "appCode" | "createdAtMs" | "updatedAtMs" | "whitelistEnabled">;
|
||||
|
||||
export interface GameCatalogQuery {
|
||||
platformCode?: string;
|
||||
@ -338,6 +349,40 @@ export function deleteGameCatalog(gameId: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export function setGameWhitelistEnabled(gameId: string, enabled: boolean): Promise<GameCatalogDto> {
|
||||
const endpoint = API_ENDPOINTS.setGameWhitelistEnabled;
|
||||
return apiRequest<GameCatalogDto, { enabled: boolean }>(
|
||||
apiEndpointPath(API_OPERATIONS.setGameWhitelistEnabled, { game_id: gameId }),
|
||||
{ body: { enabled }, method: endpoint.method },
|
||||
).then(normalizeCatalog);
|
||||
}
|
||||
|
||||
export function listGameWhitelistUsers(
|
||||
gameId: string,
|
||||
): Promise<{ items: GameWhitelistUserDto[]; serverTimeMs?: number }> {
|
||||
const endpoint = API_ENDPOINTS.listGameWhitelistUsers;
|
||||
return apiRequest<{ items: GameWhitelistUserDto[]; serverTimeMs?: number }>(
|
||||
apiEndpointPath(API_OPERATIONS.listGameWhitelistUsers, { game_id: gameId }),
|
||||
{ method: endpoint.method, query: { pageSize: 200 } },
|
||||
).then((result) => ({ ...result, items: (result.items || []).map(normalizeGameWhitelistUser) }));
|
||||
}
|
||||
|
||||
export function addGameWhitelistUser(gameId: string, userId: string): Promise<GameWhitelistUserDto> {
|
||||
const endpoint = API_ENDPOINTS.addGameWhitelistUser;
|
||||
return apiRequest<GameWhitelistUserDto, { userId: string }>(
|
||||
apiEndpointPath(API_OPERATIONS.addGameWhitelistUser, { game_id: gameId }),
|
||||
{ body: { userId }, method: endpoint.method },
|
||||
).then(normalizeGameWhitelistUser);
|
||||
}
|
||||
|
||||
export function deleteGameWhitelistUser(gameId: string, userId: string): Promise<void> {
|
||||
const endpoint = API_ENDPOINTS.deleteGameWhitelistUser;
|
||||
return apiRequest<void>(
|
||||
apiEndpointPath(API_OPERATIONS.deleteGameWhitelistUser, { game_id: gameId, user_id: userId }),
|
||||
{ method: endpoint.method },
|
||||
);
|
||||
}
|
||||
|
||||
export function syncGamePlatformCatalog(platformCode: string, payload: GameSyncPayload = {}): Promise<GameSyncResult> {
|
||||
return apiRequest<GameSyncResult, GameSyncPayload>(
|
||||
`/v1/admin/game/platforms/${encodeURIComponent(platformCode)}/sync-games`,
|
||||
@ -559,6 +604,19 @@ function normalizeCatalog(game: Partial<GameCatalogDto>): GameCatalogDto {
|
||||
tags: Array.isArray(game.tags) ? game.tags.map(String).filter(Boolean) : [],
|
||||
createdAtMs: numberValue(game.createdAtMs),
|
||||
updatedAtMs: numberValue(game.updatedAtMs),
|
||||
whitelistEnabled: game.whitelistEnabled === true,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGameWhitelistUser(user: Partial<GameWhitelistUserDto>): GameWhitelistUserDto {
|
||||
return {
|
||||
userId: stringValue(user.userId),
|
||||
displayUserId: stringValue(user.displayUserId),
|
||||
username: stringValue(user.username),
|
||||
avatar: stringValue(user.avatar),
|
||||
status: stringValue(user.status),
|
||||
createdByAdminId: numberValue(user.createdByAdminId),
|
||||
createdAtMs: numberValue(user.createdAtMs),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
119
src/features/games/components/GameWhitelistDialog.jsx
Normal file
119
src/features/games/components/GameWhitelistDialog.jsx
Normal file
@ -0,0 +1,119 @@
|
||||
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||||
import PersonAddAltOutlined from "@mui/icons-material/PersonAddAltOutlined";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||
import { Button } from "@/shared/ui/Button.jsx";
|
||||
import { AdminActionIconButton } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import styles from "@/features/games/games.module.css";
|
||||
|
||||
export function GameWhitelistDialog({ open, page, onClose }) {
|
||||
const game = page.whitelistGame;
|
||||
const enabledLoading = page.loadingAction === `whitelist-enabled-${game?.gameId}`;
|
||||
const addLoading = page.loadingAction === `whitelist-add-${game?.gameId}`;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className={styles.whitelistDialog}
|
||||
maxWidth={false}
|
||||
open={open}
|
||||
slotProps={{ paper: { className: styles.whitelistDialogPaper } }}
|
||||
onClose={onClose}
|
||||
>
|
||||
<DialogTitle>游戏白名单 · {game?.gameName || game?.gameId || ""}</DialogTitle>
|
||||
<DialogContent className={styles.whitelistDialogContent}>
|
||||
<div className={styles.whitelistPolicyRow}>
|
||||
<div className={styles.stack}>
|
||||
<span className={styles.name}>仅对白名单用户开放</span>
|
||||
<span className={styles.meta}>开启后,非成员不会在游戏列表看到此游戏,也不能直接启动。</span>
|
||||
</div>
|
||||
<AdminSwitch
|
||||
checked={game?.whitelistEnabled === true}
|
||||
checkedLabel="开启"
|
||||
disabled={!page.abilities.canUpdate || enabledLoading}
|
||||
label="游戏白名单状态"
|
||||
size="small"
|
||||
uncheckedLabel="关闭"
|
||||
onChange={(event) => page.changeWhitelistEnabled(event.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form className={styles.whitelistSearchRow} onSubmit={page.addWhitelistUser}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="搜索用户 ID"
|
||||
placeholder="输入内部用户 ID"
|
||||
slotProps={{ htmlInput: { inputMode: "numeric" } }}
|
||||
value={page.whitelistUserId}
|
||||
onChange={(event) => page.setWhitelistUserId(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
disabled={!page.abilities.canUpdate || addLoading || !page.whitelistUserId.trim()}
|
||||
startIcon={
|
||||
addLoading ? <CircularProgress color="inherit" size={16} /> : <PersonAddAltOutlined />
|
||||
}
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className={styles.whitelistTable}>
|
||||
<div className={styles.whitelistTableHeader}>
|
||||
<span>用户</span>
|
||||
<span>用户 ID</span>
|
||||
<span>状态</span>
|
||||
<span>加入时间</span>
|
||||
<span>操作</span>
|
||||
</div>
|
||||
{page.whitelistLoading ? (
|
||||
<div className={styles.whitelistEmpty}>
|
||||
<CircularProgress size={24} />
|
||||
</div>
|
||||
) : page.whitelistItems.length > 0 ? (
|
||||
page.whitelistItems.map((item) => (
|
||||
<div className={styles.whitelistTableRow} key={item.userId}>
|
||||
<div className={styles.whitelistUserIdentity}>
|
||||
<Avatar className={styles.whitelistAvatar} src={item.avatar || undefined}>
|
||||
{(item.username || item.displayUserId || item.userId).slice(0, 1)}
|
||||
</Avatar>
|
||||
<span className={styles.name}>{item.username || "-"}</span>
|
||||
</div>
|
||||
<div className={styles.stack}>
|
||||
<span>{item.userId}</span>
|
||||
{item.displayUserId ? (
|
||||
<span className={styles.meta}>{item.displayUserId}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<span>{item.status || "-"}</span>
|
||||
<span>{formatMillis(item.createdAtMs)}</span>
|
||||
<AdminActionIconButton
|
||||
disabled={
|
||||
!page.abilities.canUpdate ||
|
||||
page.loadingAction === `whitelist-delete-${item.userId}`
|
||||
}
|
||||
label="移出白名单"
|
||||
onClick={() => page.removeWhitelistUser(item)}
|
||||
>
|
||||
<DeleteOutlineOutlined fontSize="small" />
|
||||
</AdminActionIconButton>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.whitelistEmpty}>当前没有白名单用户</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>关闭</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
48
src/features/games/components/GameWhitelistDialog.test.jsx
Normal file
48
src/features/games/components/GameWhitelistDialog.test.jsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { AppProviders } from "@/app/providers.jsx";
|
||||
import { GameWhitelistDialog } from "./GameWhitelistDialog.jsx";
|
||||
|
||||
test("renders current members and exposes whitelist toggle, add and remove actions", async () => {
|
||||
const user = userEvent.setup();
|
||||
const page = {
|
||||
abilities: { canUpdate: true },
|
||||
addWhitelistUser: vi.fn((event) => event.preventDefault()),
|
||||
changeWhitelistEnabled: vi.fn(),
|
||||
loadingAction: "",
|
||||
removeWhitelistUser: vi.fn(),
|
||||
setWhitelistUserId: vi.fn(),
|
||||
whitelistGame: { gameId: "private_slot", gameName: "Private Slot", whitelistEnabled: true },
|
||||
whitelistItems: [
|
||||
{
|
||||
createdAtMs: 1700000000000,
|
||||
displayUserId: "90001",
|
||||
status: "active",
|
||||
userId: "42",
|
||||
username: "Alice",
|
||||
},
|
||||
],
|
||||
whitelistLoading: false,
|
||||
whitelistUserId: "42",
|
||||
};
|
||||
|
||||
render(
|
||||
<AppProviders>
|
||||
<GameWhitelistDialog open page={page} onClose={vi.fn()} />
|
||||
</AppProviders>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("游戏白名单 · Private Slot")).toBeInTheDocument();
|
||||
expect(screen.getByText("Alice")).toBeInTheDocument();
|
||||
expect(screen.getByText("90001")).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("switch", { name: "游戏白名单状态" }));
|
||||
expect(page.changeWhitelistEnabled).toHaveBeenCalledWith(false);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "添加" }));
|
||||
expect(page.addWhitelistUser).toHaveBeenCalledOnce();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "移出白名单" }));
|
||||
expect(page.removeWhitelistUser).toHaveBeenCalledWith(page.whitelistItems[0]);
|
||||
});
|
||||
@ -334,3 +334,98 @@
|
||||
max-width: 360px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
.whitelistDialogPaper {
|
||||
width: min(980px, calc(100vw - 32px));
|
||||
max-height: min(760px, calc(100vh - 32px));
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.whitelistDialogContent {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.whitelistPolicyRow,
|
||||
.whitelistSearchRow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.whitelistPolicyRow {
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-card-strong);
|
||||
}
|
||||
|
||||
.whitelistSearchRow > :last-child {
|
||||
min-width: 104px;
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.whitelistTable {
|
||||
overflow: auto;
|
||||
max-height: 440px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.whitelistTableHeader,
|
||||
.whitelistTableRow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 1.2fr) minmax(150px, 1fr) 88px 180px 64px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-width: 760px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.whitelistTableHeader {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-table-head);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.whitelistTableRow {
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.whitelistTableRow:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.whitelistUserIdentity {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.whitelistAvatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.whitelistEmpty {
|
||||
display: flex;
|
||||
min-height: 160px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.whitelistPolicyRow,
|
||||
.whitelistSearchRow {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
addGameWhitelistUser,
|
||||
deleteGameCatalog,
|
||||
deleteGameWhitelistUser,
|
||||
listGameCatalog,
|
||||
listGamePlatforms,
|
||||
listGameWhitelistUsers,
|
||||
setGameStatus,
|
||||
setGameWhitelistEnabled,
|
||||
syncGamePlatformCatalog,
|
||||
upsertGameCatalog,
|
||||
upsertGamePlatform,
|
||||
@ -89,6 +93,10 @@ export function useGamesPage() {
|
||||
const [syncPlatform, setSyncPlatform] = useState(null);
|
||||
const [syncCandidates, setSyncCandidates] = useState([]);
|
||||
const [selectedSyncGameIds, setSelectedSyncGameIds] = useState([]);
|
||||
const [whitelistGame, setWhitelistGame] = useState(null);
|
||||
const [whitelistItems, setWhitelistItems] = useState([]);
|
||||
const [whitelistUserId, setWhitelistUserId] = useState("");
|
||||
const [whitelistLoading, setWhitelistLoading] = useState(false);
|
||||
const catalogRequestRef = useRef(0);
|
||||
|
||||
const platformsQueryFn = useCallback(() => listGamePlatforms(), []);
|
||||
@ -208,6 +216,29 @@ export function useGamesPage() {
|
||||
setActiveAction("edit-game");
|
||||
};
|
||||
|
||||
const loadWhitelist = async (game) => {
|
||||
if (!game?.gameId) {
|
||||
return;
|
||||
}
|
||||
setWhitelistLoading(true);
|
||||
try {
|
||||
const result = await listGameWhitelistUsers(game.gameId);
|
||||
setWhitelistItems(result.items || []);
|
||||
} catch (err) {
|
||||
showToast(err.message || "加载游戏白名单失败", "error");
|
||||
} finally {
|
||||
setWhitelistLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openWhitelist = (game) => {
|
||||
setWhitelistGame(game);
|
||||
setWhitelistItems([]);
|
||||
setWhitelistUserId("");
|
||||
setActiveAction("game-whitelist");
|
||||
loadWhitelist(game);
|
||||
};
|
||||
|
||||
const openCreatePlatform = () => {
|
||||
setEditingPlatformCode("");
|
||||
setPlatformForm(defaultPlatformForm);
|
||||
@ -238,6 +269,11 @@ export function useGamesPage() {
|
||||
if (activeAction === "bridge-script") {
|
||||
setBridgeScriptForm(defaultBridgeScriptForm);
|
||||
}
|
||||
if (activeAction === "game-whitelist") {
|
||||
setWhitelistGame(null);
|
||||
setWhitelistItems([]);
|
||||
setWhitelistUserId("");
|
||||
}
|
||||
setActiveAction("");
|
||||
setEditingGameId("");
|
||||
setEditingPlatformCode("");
|
||||
@ -377,6 +413,76 @@ export function useGamesPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const changeWhitelistEnabled = async (checked) => {
|
||||
if (!whitelistGame) {
|
||||
return;
|
||||
}
|
||||
setLoadingAction(`whitelist-enabled-${whitelistGame.gameId}`);
|
||||
try {
|
||||
const updated = await setGameWhitelistEnabled(whitelistGame.gameId, checked);
|
||||
setWhitelistGame((current) =>
|
||||
current ? { ...current, whitelistEnabled: updated.whitelistEnabled } : current,
|
||||
);
|
||||
setCatalogItems((current) =>
|
||||
current.map((game) =>
|
||||
game.gameId === whitelistGame.gameId
|
||||
? { ...game, whitelistEnabled: updated.whitelistEnabled }
|
||||
: game,
|
||||
),
|
||||
);
|
||||
showToast(checked ? "游戏白名单已开启" : "游戏白名单已关闭", "success");
|
||||
} catch (err) {
|
||||
showToast(err.message || "白名单状态更新失败", "error");
|
||||
} finally {
|
||||
setLoadingAction("");
|
||||
}
|
||||
};
|
||||
|
||||
const addWhitelistUser = async (event) => {
|
||||
event.preventDefault();
|
||||
const userId = whitelistUserId.trim();
|
||||
if (!whitelistGame || !/^\d+$/.test(userId) || userId === "0") {
|
||||
showToast("请输入正确的用户 ID", "error");
|
||||
return;
|
||||
}
|
||||
setLoadingAction(`whitelist-add-${whitelistGame.gameId}`);
|
||||
try {
|
||||
await addGameWhitelistUser(whitelistGame.gameId, userId);
|
||||
setWhitelistUserId("");
|
||||
await loadWhitelist(whitelistGame);
|
||||
showToast("用户已加入白名单", "success");
|
||||
} catch (err) {
|
||||
showToast(err.message || "添加白名单用户失败", "error");
|
||||
} finally {
|
||||
setLoadingAction("");
|
||||
}
|
||||
};
|
||||
|
||||
const removeWhitelistUser = async (item) => {
|
||||
if (!whitelistGame) {
|
||||
return;
|
||||
}
|
||||
const ok = await confirm({
|
||||
confirmText: "移除",
|
||||
message: `移除用户 ${item.displayUserId || item.userId} 后,该用户将看不到且无法启动此游戏。`,
|
||||
title: "移除白名单用户",
|
||||
tone: "danger",
|
||||
});
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
setLoadingAction(`whitelist-delete-${item.userId}`);
|
||||
try {
|
||||
await deleteGameWhitelistUser(whitelistGame.gameId, item.userId);
|
||||
setWhitelistItems((current) => current.filter((user) => user.userId !== item.userId));
|
||||
showToast("用户已移出白名单", "success");
|
||||
} catch (err) {
|
||||
showToast(err.message || "移除白名单用户失败", "error");
|
||||
} finally {
|
||||
setLoadingAction("");
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPlatformGames = async (platform) => {
|
||||
setLoadingAction(`fetch-games-${platform.platformCode}`);
|
||||
try {
|
||||
@ -464,7 +570,9 @@ export function useGamesPage() {
|
||||
activeAction,
|
||||
allSyncGamesSelected,
|
||||
bridgeScriptForm,
|
||||
addWhitelistUser,
|
||||
changeGameStatus,
|
||||
changeWhitelistEnabled,
|
||||
deleteGame,
|
||||
closeAction,
|
||||
data: {
|
||||
@ -487,6 +595,7 @@ export function useGamesPage() {
|
||||
openBridgeScript,
|
||||
openEditGame,
|
||||
openEditPlatform,
|
||||
openWhitelist,
|
||||
openManagePlatforms,
|
||||
pageSize,
|
||||
platformCode,
|
||||
@ -496,6 +605,7 @@ export function useGamesPage() {
|
||||
loadNextPage,
|
||||
reload: reloadCatalog,
|
||||
resetFilters,
|
||||
removeWhitelistUser,
|
||||
resolveGameURL,
|
||||
changePageSize,
|
||||
setGameForm,
|
||||
@ -512,6 +622,11 @@ export function useGamesPage() {
|
||||
selectedSyncGameIds,
|
||||
toggleAllSyncGames,
|
||||
toggleSyncGame,
|
||||
whitelistGame,
|
||||
whitelistItems,
|
||||
whitelistLoading,
|
||||
whitelistUserId,
|
||||
setWhitelistUserId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import CodeOutlined from "@mui/icons-material/CodeOutlined";
|
||||
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||||
import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
|
||||
import GroupOutlined from "@mui/icons-material/GroupOutlined";
|
||||
import SportsEsportsOutlined from "@mui/icons-material/SportsEsportsOutlined";
|
||||
import SyncOutlined from "@mui/icons-material/SyncOutlined";
|
||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||
@ -25,6 +26,7 @@ import { createOptionsColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import { useGamesPage } from "@/features/games/hooks/useGamesPage.js";
|
||||
import { GameWhitelistDialog } from "@/features/games/components/GameWhitelistDialog.jsx";
|
||||
import styles from "@/features/games/games.module.css";
|
||||
|
||||
const statusOptions = [
|
||||
@ -105,7 +107,7 @@ const baseColumns = [
|
||||
{
|
||||
key: "actions",
|
||||
label: "操作",
|
||||
width: "minmax(112px, 0.5fr)",
|
||||
width: "minmax(152px, 0.65fr)",
|
||||
},
|
||||
];
|
||||
|
||||
@ -231,6 +233,7 @@ export function GameListPage() {
|
||||
onClose={page.closeAction}
|
||||
/>
|
||||
<ProviderGameListDialog open={page.activeAction === "sync-games"} page={page} onClose={page.closeAction} />
|
||||
<GameWhitelistDialog open={page.activeAction === "game-whitelist"} page={page} onClose={page.closeAction} />
|
||||
</AdminListPage>
|
||||
);
|
||||
}
|
||||
@ -269,6 +272,15 @@ function GameStatusSwitch({ page, game }) {
|
||||
function GameRowActions({ page, game }) {
|
||||
return (
|
||||
<AdminRowActions>
|
||||
<AdminActionIconButton
|
||||
disabled={!page.abilities.canView}
|
||||
label="白名单"
|
||||
primary={game.whitelistEnabled === true}
|
||||
tooltip={game.whitelistEnabled ? "白名单已开启" : "管理白名单"}
|
||||
onClick={() => page.openWhitelist(game)}
|
||||
>
|
||||
<GroupOutlined fontSize="small" />
|
||||
</AdminActionIconButton>
|
||||
<AdminActionIconButton
|
||||
disabled={!page.abilities.canUpdate}
|
||||
label="编辑游戏"
|
||||
|
||||
@ -13,6 +13,7 @@ export interface ApiEndpoint {
|
||||
}
|
||||
|
||||
export const API_OPERATIONS = {
|
||||
addGameWhitelistUser: "addGameWhitelistUser",
|
||||
adjustDicePool: "adjustDicePool",
|
||||
adminAddAgencyHost: "adminAddAgencyHost",
|
||||
appAdjustUserLevels: "appAdjustUserLevels",
|
||||
@ -88,6 +89,7 @@ export const API_OPERATIONS = {
|
||||
deleteCountry: "deleteCountry",
|
||||
deleteDiceRobot: "deleteDiceRobot",
|
||||
deleteExploreTab: "deleteExploreTab",
|
||||
deleteGameWhitelistUser: "deleteGameWhitelistUser",
|
||||
deleteGift: "deleteGift",
|
||||
deleteH5Link: "deleteH5Link",
|
||||
deleteHostAgencyPolicy: "deleteHostAgencyPolicy",
|
||||
@ -197,6 +199,7 @@ export const API_OPERATIONS = {
|
||||
listFinanceCoinSellerRechargeOrders: "listFinanceCoinSellerRechargeOrders",
|
||||
listFinanceWithdrawalApplications: "listFinanceWithdrawalApplications",
|
||||
listFirstRechargeRewardClaims: "listFirstRechargeRewardClaims",
|
||||
listGameWhitelistUsers: "listGameWhitelistUsers",
|
||||
listGifts: "listGifts",
|
||||
listGiftTypes: "listGiftTypes",
|
||||
listH5Links: "listH5Links",
|
||||
@ -295,6 +298,7 @@ export const API_OPERATIONS = {
|
||||
setCoinSellerStatus: "setCoinSellerStatus",
|
||||
setDiceRobotStatus: "setDiceRobotStatus",
|
||||
setGameStatus: "setGameStatus",
|
||||
setGameWhitelistEnabled: "setGameWhitelistEnabled",
|
||||
setPrettyIdStatus: "setPrettyIdStatus",
|
||||
setTaskDefinitionStatus: "setTaskDefinitionStatus",
|
||||
setThirdPartyPaymentMethodStatus: "setThirdPartyPaymentMethodStatus",
|
||||
@ -375,6 +379,13 @@ export const API_OPERATIONS = {
|
||||
export type ApiOperationId = (typeof API_OPERATIONS)[keyof typeof API_OPERATIONS];
|
||||
|
||||
export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
addGameWhitelistUser: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.addGameWhitelistUser,
|
||||
path: "/v1/admin/game/games/{game_id}/whitelist/users",
|
||||
permission: "game:update",
|
||||
permissions: ["game:update"]
|
||||
},
|
||||
adjustDicePool: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.adjustDicePool,
|
||||
@ -898,6 +909,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "app-config:update",
|
||||
permissions: ["app-config:update"]
|
||||
},
|
||||
deleteGameWhitelistUser: {
|
||||
method: "DELETE",
|
||||
operationId: API_OPERATIONS.deleteGameWhitelistUser,
|
||||
path: "/v1/admin/game/games/{game_id}/whitelist/users/{user_id}",
|
||||
permission: "game:update",
|
||||
permissions: ["game:update"]
|
||||
},
|
||||
deleteGift: {
|
||||
method: "DELETE",
|
||||
operationId: API_OPERATIONS.deleteGift,
|
||||
@ -1659,6 +1677,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "first-recharge-reward:view",
|
||||
permissions: ["first-recharge-reward:view"]
|
||||
},
|
||||
listGameWhitelistUsers: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listGameWhitelistUsers,
|
||||
path: "/v1/admin/game/games/{game_id}/whitelist/users",
|
||||
permission: "game:view",
|
||||
permissions: ["game:view"]
|
||||
},
|
||||
listGifts: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listGifts,
|
||||
@ -2333,6 +2358,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "game:status",
|
||||
permissions: ["game:status"]
|
||||
},
|
||||
setGameWhitelistEnabled: {
|
||||
method: "PATCH",
|
||||
operationId: API_OPERATIONS.setGameWhitelistEnabled,
|
||||
path: "/v1/admin/game/games/{game_id}/whitelist",
|
||||
permission: "game:update",
|
||||
permissions: ["game:update"]
|
||||
},
|
||||
setPrettyIdStatus: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.setPrettyIdStatus,
|
||||
|
||||
105
src/shared/api/generated/schema.d.ts
vendored
105
src/shared/api/generated/schema.d.ts
vendored
@ -1516,6 +1516,54 @@ export interface paths {
|
||||
patch: operations["setGameStatus"];
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/game/games/{game_id}/whitelist": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch: operations["setGameWhitelistEnabled"];
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/game/games/{game_id}/whitelist/users": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listGameWhitelistUsers"];
|
||||
put?: never;
|
||||
post: operations["addGameWhitelistUser"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/game/games/{game_id}/whitelist/users/{user_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete: operations["deleteGameWhitelistUser"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/game/platforms": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -8466,6 +8514,63 @@ export interface operations {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
setGameWhitelistEnabled: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
game_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
listGameWhitelistUsers: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
game_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
addGameWhitelistUser: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
game_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
deleteGameWhitelistUser: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
game_id: string;
|
||||
user_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
listPlatforms: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user