机器人房
This commit is contained in:
parent
e25258f233
commit
7c4bdf8d77
@ -3138,6 +3138,84 @@
|
||||
"x-permissions": ["room-pin:cancel"]
|
||||
}
|
||||
},
|
||||
"/admin/rooms/robot-rooms": {
|
||||
"get": {
|
||||
"operationId": "listRobotRooms",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "room-robot:view",
|
||||
"x-permissions": ["room-robot:view"]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "createRobotRoom",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "room-robot:create",
|
||||
"x-permissions": ["room-robot:create"]
|
||||
}
|
||||
},
|
||||
"/admin/rooms/robot-rooms/available-robots": {
|
||||
"get": {
|
||||
"operationId": "listAvailableRoomRobots",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "room-robot:view",
|
||||
"x-permissions": ["room-robot:view"]
|
||||
}
|
||||
},
|
||||
"/admin/rooms/robot-rooms/{room_id}/start": {
|
||||
"post": {
|
||||
"operationId": "startRobotRoom",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "room_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "room-robot:update",
|
||||
"x-permissions": ["room-robot:update"]
|
||||
}
|
||||
},
|
||||
"/admin/rooms/robot-rooms/{room_id}/stop": {
|
||||
"post": {
|
||||
"operationId": "stopRobotRoom",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "room_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "room-robot:update",
|
||||
"x-permissions": ["room-robot:update"]
|
||||
}
|
||||
},
|
||||
"/admin/users/level-config": {
|
||||
"get": {
|
||||
"operationId": "listLevelConfig",
|
||||
|
||||
@ -141,6 +141,7 @@ export const fallbackNavigation = [
|
||||
routeNavItem("room-list", { icon: BedroomParentOutlined }),
|
||||
routeNavItem("room-pins", { icon: PushPinOutlined }),
|
||||
routeNavItem("room-config", { icon: SettingsApplicationsOutlined }),
|
||||
routeNavItem("room-robots", { icon: GroupsOutlined }),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -97,6 +97,9 @@ export const PERMISSIONS = {
|
||||
roomPinCancel: "room-pin:cancel",
|
||||
roomConfigView: "room-config:view",
|
||||
roomConfigUpdate: "room-config:update",
|
||||
roomRobotView: "room-robot:view",
|
||||
roomRobotCreate: "room-robot:create",
|
||||
roomRobotUpdate: "room-robot:update",
|
||||
appConfigView: "app-config:view",
|
||||
appConfigUpdate: "app-config:update",
|
||||
appVersionView: "app-version:view",
|
||||
@ -183,6 +186,7 @@ export const MENU_CODES = {
|
||||
roomList: "room-list",
|
||||
roomPins: "room-pins",
|
||||
roomConfig: "room-config",
|
||||
roomRobots: "room-robots",
|
||||
appConfig: "app-config",
|
||||
appConfigH5: "app-config-h5",
|
||||
appConfigBanners: "app-config-banners",
|
||||
|
||||
@ -9,6 +9,9 @@ import type {
|
||||
RoomDto,
|
||||
RoomPinDto,
|
||||
RoomPinPayload,
|
||||
AvailableRoomRobotDto,
|
||||
RobotRoomDto,
|
||||
RobotRoomPayload,
|
||||
RoomUpdatePayload,
|
||||
} from "@/shared/api/types";
|
||||
|
||||
@ -72,3 +75,40 @@ export function updateRoomConfig(payload: RoomConfigPayload): Promise<RoomConfig
|
||||
method: endpoint.method,
|
||||
});
|
||||
}
|
||||
|
||||
export function listRobotRooms(query: PageQuery = {}): Promise<ApiPage<RobotRoomDto>> {
|
||||
const endpoint = API_ENDPOINTS.listRobotRooms;
|
||||
return apiRequest<ApiPage<RobotRoomDto>>(apiEndpointPath(API_OPERATIONS.listRobotRooms), {
|
||||
method: endpoint.method,
|
||||
query,
|
||||
});
|
||||
}
|
||||
|
||||
export function listAvailableRoomRobots(): Promise<AvailableRoomRobotDto[]> {
|
||||
const endpoint = API_ENDPOINTS.listAvailableRoomRobots;
|
||||
return apiRequest<AvailableRoomRobotDto[]>(apiEndpointPath(API_OPERATIONS.listAvailableRoomRobots), {
|
||||
method: endpoint.method,
|
||||
});
|
||||
}
|
||||
|
||||
export function createRobotRoom(payload: RobotRoomPayload): Promise<RobotRoomDto> {
|
||||
const endpoint = API_ENDPOINTS.createRobotRoom;
|
||||
return apiRequest<RobotRoomDto, RobotRoomPayload>(apiEndpointPath(API_OPERATIONS.createRobotRoom), {
|
||||
body: payload,
|
||||
method: endpoint.method,
|
||||
});
|
||||
}
|
||||
|
||||
export function startRobotRoom(roomId: EntityId): Promise<RobotRoomDto> {
|
||||
const endpoint = API_ENDPOINTS.startRobotRoom;
|
||||
return apiRequest<RobotRoomDto>(apiEndpointPath(API_OPERATIONS.startRobotRoom, { room_id: roomId }), {
|
||||
method: endpoint.method,
|
||||
});
|
||||
}
|
||||
|
||||
export function stopRobotRoom(roomId: EntityId): Promise<RobotRoomDto> {
|
||||
const endpoint = API_ENDPOINTS.stopRobotRoom;
|
||||
return apiRequest<RobotRoomDto>(apiEndpointPath(API_OPERATIONS.stopRobotRoom, { room_id: roomId }), {
|
||||
method: endpoint.method,
|
||||
});
|
||||
}
|
||||
|
||||
242
src/features/rooms/hooks/useRobotRoomsPage.js
Normal file
242
src/features/rooms/hooks/useRobotRoomsPage.js
Normal file
@ -0,0 +1,242 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { listGifts } from "@/features/resources/api";
|
||||
import {
|
||||
createRobotRoom,
|
||||
listAvailableRoomRobots,
|
||||
listRobotRooms,
|
||||
startRobotRoom,
|
||||
stopRobotRoom,
|
||||
} from "@/features/rooms/api";
|
||||
import { useRoomAbilities } from "@/features/rooms/permissions.js";
|
||||
import { robotRoomCreateSchema } from "@/features/rooms/schema";
|
||||
import { parseForm } from "@/shared/forms/validation";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
|
||||
const pageSize = 50;
|
||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
candidateRobotUserIds: [],
|
||||
giftIds: [],
|
||||
luckyComboMax: "10000",
|
||||
luckyComboMin: "100",
|
||||
luckyGiftIds: [],
|
||||
luckyPauseMaxMs: "20000",
|
||||
luckyPauseMinMs: "5000",
|
||||
maxRobotCount: "8",
|
||||
minRobotCount: "6",
|
||||
normalGiftIntervalMs: "10000",
|
||||
ownerRobotUserId: "",
|
||||
roomName: "",
|
||||
visibleRegionId: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function useRobotRoomsPage() {
|
||||
const abilities = useRoomAbilities();
|
||||
const { showToast } = useToast();
|
||||
const [activeAction, setActiveAction] = useState("");
|
||||
const [availableRobotsState, setAvailableRobotsState] = useState({ error: "", loading: false, options: [] });
|
||||
const [candidatesTouched, setCandidatesTouched] = useState(false);
|
||||
const [form, setForm] = useState(emptyForm);
|
||||
const [giftOptionsState, setGiftOptionsState] = useState({ error: "", loading: false, options: [] });
|
||||
const [loadingAction, setLoadingAction] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [status, setStatus] = useState("active");
|
||||
const filters = useMemo(() => ({ status }), [status]);
|
||||
const {
|
||||
data = emptyData,
|
||||
error,
|
||||
loading,
|
||||
reload,
|
||||
} = usePaginatedQuery({
|
||||
errorMessage: "加载机器人房间列表失败",
|
||||
fetcher: listRobotRooms,
|
||||
filters,
|
||||
page,
|
||||
pageSize,
|
||||
queryKey: ["robot-rooms", filters, page],
|
||||
});
|
||||
|
||||
const loadAvailableRobots = async () => {
|
||||
setAvailableRobotsState((current) => ({ ...current, error: "", loading: true }));
|
||||
try {
|
||||
const options = await listAvailableRoomRobots();
|
||||
setAvailableRobotsState({ error: "", loading: false, options: options || [] });
|
||||
} catch (err) {
|
||||
setAvailableRobotsState({ error: err.message || "加载可用机器人失败", loading: false, options: [] });
|
||||
}
|
||||
};
|
||||
|
||||
const loadGiftOptions = async () => {
|
||||
setGiftOptionsState((current) => ({ ...current, error: "", loading: true }));
|
||||
try {
|
||||
const result = await listGifts({ page: 1, page_size: 200, status: "active" });
|
||||
setGiftOptionsState({ error: "", loading: false, options: result?.items || [] });
|
||||
} catch (err) {
|
||||
setGiftOptionsState({ error: err.message || "加载礼物列表失败", loading: false, options: [] });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (activeAction !== "create") {
|
||||
return;
|
||||
}
|
||||
loadAvailableRobots();
|
||||
loadGiftOptions();
|
||||
}, [activeAction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeAction !== "create" || candidatesTouched) {
|
||||
return;
|
||||
}
|
||||
const ids = availableRobotsState.options.map((item) => item.userId).filter(Boolean);
|
||||
if (ids.length > 0) {
|
||||
setForm((current) => ({ ...current, candidateRobotUserIds: ids }));
|
||||
}
|
||||
}, [activeAction, availableRobotsState.options, candidatesTouched]);
|
||||
|
||||
const changeStatus = (value) => {
|
||||
setStatus(value || "active");
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setStatus("active");
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
setActiveAction("create");
|
||||
setCandidatesTouched(false);
|
||||
setForm(emptyForm());
|
||||
setAvailableRobotsState({ error: "", loading: false, options: [] });
|
||||
setGiftOptionsState({ error: "", loading: false, options: [] });
|
||||
};
|
||||
|
||||
const closeAction = () => {
|
||||
setActiveAction("");
|
||||
};
|
||||
|
||||
const submitCreate = async (event) => {
|
||||
event.preventDefault();
|
||||
await runAction("create", "机器人房间已创建", async () => {
|
||||
const payload = parseForm(robotRoomCreateSchema, form);
|
||||
await createRobotRoom(payload);
|
||||
closeAction();
|
||||
await reload();
|
||||
});
|
||||
};
|
||||
|
||||
const setRoomStatus = async (room) => {
|
||||
if (!room?.roomId || !abilities.canRobotUpdate) {
|
||||
return;
|
||||
}
|
||||
const nextActive = room.status !== "active";
|
||||
await runAction(`status-${room.roomId}`, nextActive ? "机器人房间已启动" : "机器人房间已停止", async () => {
|
||||
if (nextActive) {
|
||||
await startRobotRoom(room.roomId);
|
||||
} else {
|
||||
await stopRobotRoom(room.roomId);
|
||||
}
|
||||
await reload();
|
||||
});
|
||||
};
|
||||
|
||||
const selectOwnerRobot = (robot) => {
|
||||
setForm((current) => ({ ...current, ownerRobotUserId: robot?.userId || "" }));
|
||||
};
|
||||
|
||||
const selectCandidateRobots = (robots) => {
|
||||
setCandidatesTouched(true);
|
||||
setForm((current) => ({ ...current, candidateRobotUserIds: robots.map((robot) => robot.userId).filter(Boolean) }));
|
||||
};
|
||||
|
||||
const selectGiftIds = (key, gifts) => {
|
||||
setForm((current) => ({ ...current, [key]: gifts.map(giftId).filter(Boolean) }));
|
||||
};
|
||||
|
||||
const runAction = async (action, successMessage, submitter) => {
|
||||
setLoadingAction(action);
|
||||
try {
|
||||
await submitter();
|
||||
showToast(successMessage, "success");
|
||||
} catch (err) {
|
||||
showToast(err.message || "操作失败", "error");
|
||||
} finally {
|
||||
setLoadingAction("");
|
||||
}
|
||||
};
|
||||
|
||||
const selectedOwnerRobot = useMemo(
|
||||
() => availableRobotsState.options.find((item) => item.userId === String(form.ownerRobotUserId)) || null,
|
||||
[availableRobotsState.options, form.ownerRobotUserId],
|
||||
);
|
||||
const selectedCandidateRobots = useMemo(() => {
|
||||
const selected = new Set((form.candidateRobotUserIds || []).map(String));
|
||||
return availableRobotsState.options.filter((item) => selected.has(String(item.userId)));
|
||||
}, [availableRobotsState.options, form.candidateRobotUserIds]);
|
||||
const selectedGiftOptions = useMemo(() => {
|
||||
const selected = new Set((form.giftIds || []).map(String));
|
||||
return giftOptionsState.options.filter((item) => selected.has(giftId(item)));
|
||||
}, [form.giftIds, giftOptionsState.options]);
|
||||
const selectedLuckyGiftOptions = useMemo(() => {
|
||||
const selected = new Set((form.luckyGiftIds || []).map(String));
|
||||
return giftOptionsState.options.filter((item) => selected.has(giftId(item)));
|
||||
}, [form.luckyGiftIds, giftOptionsState.options]);
|
||||
|
||||
return {
|
||||
abilities,
|
||||
activeAction,
|
||||
availableRobotsError: availableRobotsState.error,
|
||||
availableRobotsLoading: availableRobotsState.loading,
|
||||
availableRobots: availableRobotsState.options,
|
||||
changeStatus,
|
||||
closeAction,
|
||||
data,
|
||||
error,
|
||||
form,
|
||||
giftOptions: giftOptionsState.options,
|
||||
giftOptionsError: giftOptionsState.error,
|
||||
giftOptionsLoading: giftOptionsState.loading,
|
||||
loading,
|
||||
loadingAction,
|
||||
openCreate,
|
||||
page,
|
||||
reload,
|
||||
resetFilters,
|
||||
selectCandidateRobots,
|
||||
selectGiftIds,
|
||||
selectOwnerRobot,
|
||||
selectedCandidateRobots,
|
||||
selectedGiftOptions,
|
||||
selectedLuckyGiftOptions,
|
||||
selectedOwnerRobot,
|
||||
setForm,
|
||||
setPage,
|
||||
setRoomStatus,
|
||||
status,
|
||||
submitCreate,
|
||||
};
|
||||
}
|
||||
|
||||
export function robotOptionLabel(robot) {
|
||||
if (!robot) {
|
||||
return "";
|
||||
}
|
||||
const user = robot.user || {};
|
||||
return [user.username, user.displayUserId, robot.userId].filter(Boolean).join(" / ");
|
||||
}
|
||||
|
||||
export function giftId(gift) {
|
||||
return String(gift?.giftId || "");
|
||||
}
|
||||
|
||||
export function giftOptionLabel(gift) {
|
||||
if (!gift) {
|
||||
return "";
|
||||
}
|
||||
return [gift.name, gift.giftId].filter(Boolean).join(" / ");
|
||||
}
|
||||
365
src/features/rooms/pages/RoomRobotPage.jsx
Normal file
365
src/features/rooms/pages/RoomRobotPage.jsx
Normal file
@ -0,0 +1,365 @@
|
||||
import AddOutlined from "@mui/icons-material/AddOutlined";
|
||||
import BedroomParentOutlined from "@mui/icons-material/BedroomParentOutlined";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { Button } from "@/shared/ui/Button.jsx";
|
||||
import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx";
|
||||
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||
import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { createOptionsColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import {
|
||||
giftId,
|
||||
giftOptionLabel,
|
||||
robotOptionLabel,
|
||||
useRobotRoomsPage,
|
||||
} from "@/features/rooms/hooks/useRobotRoomsPage.js";
|
||||
import styles from "@/features/rooms/rooms.module.css";
|
||||
|
||||
const statusOptions = [
|
||||
{ label: "运行中", value: "active" },
|
||||
{ label: "已停止", value: "stopped" },
|
||||
{ label: "全部状态", value: "all" },
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: "room",
|
||||
label: "房间信息",
|
||||
width: "minmax(280px, 1.4fr)",
|
||||
render: (room) => <RobotRoomIdentity room={room} />,
|
||||
},
|
||||
{
|
||||
key: "owner",
|
||||
label: "房主机器人",
|
||||
width: "minmax(220px, 1fr)",
|
||||
render: (room) => <RobotIdentity user={room.owner} fallbackId={room.ownerRobotUserId} />,
|
||||
},
|
||||
{
|
||||
key: "robots",
|
||||
label: "进房机器人",
|
||||
width: "minmax(120px, 0.5fr)",
|
||||
render: (room) => Number(room.robotUserIds?.length || room.robots?.length || 0).toLocaleString("zh-CN"),
|
||||
},
|
||||
{
|
||||
key: "normalRule",
|
||||
label: "普通礼物",
|
||||
width: "minmax(190px, 0.8fr)",
|
||||
render: (room) => (
|
||||
<RuleStack
|
||||
rows={[
|
||||
`${room.giftRule?.giftIds?.length || 0} 个礼物`,
|
||||
`${formatSeconds(room.giftRule?.normalGiftIntervalMs)} / 次`,
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "luckyRule",
|
||||
label: "幸运礼物",
|
||||
width: "minmax(220px, 0.9fr)",
|
||||
render: (room) => (
|
||||
<RuleStack
|
||||
rows={[
|
||||
`${room.giftRule?.luckyGiftIds?.length || 0} 个礼物`,
|
||||
`${room.giftRule?.luckyComboMin || 0}-${room.giftRule?.luckyComboMax || 0} 连击`,
|
||||
`${formatSeconds(room.giftRule?.luckyPauseMinMs)}-${formatSeconds(room.giftRule?.luckyPauseMaxMs)} 间隔`,
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "创建时间",
|
||||
width: "minmax(170px, 0.8fr)",
|
||||
render: (room) => formatMillis(room.createdAtMs),
|
||||
},
|
||||
];
|
||||
|
||||
export function RoomRobotPage() {
|
||||
const page = useRobotRoomsPage();
|
||||
const items = page.data.items || [];
|
||||
const total = page.data.total || 0;
|
||||
const tableColumns = [
|
||||
...columns.map((column) =>
|
||||
column.key === "createdAt"
|
||||
? {
|
||||
...column,
|
||||
filter: createOptionsColumnFilter({
|
||||
options: statusOptions,
|
||||
placeholder: "房间状态",
|
||||
value: page.status,
|
||||
onChange: page.changeStatus,
|
||||
}),
|
||||
}
|
||||
: column,
|
||||
),
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
width: "minmax(112px, 0.5fr)",
|
||||
render: (room) => <RobotRoomStatus page={page} room={room} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className={styles.root}>
|
||||
<div className={styles.contentPanel}>
|
||||
<div className={styles.toolbar}>
|
||||
<section className={styles.filters}>
|
||||
<AdminFilterResetButton disabled={page.status === "active"} onClick={page.resetFilters} />
|
||||
</section>
|
||||
{page.abilities.canRobotCreate ? (
|
||||
<div className={styles.toolbarActions}>
|
||||
<Button startIcon={<AddOutlined fontSize="small" />} variant="primary" onClick={page.openCreate}>
|
||||
增加机器人房间
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||
<div className={styles.listBlock}>
|
||||
<DataTable
|
||||
columns={tableColumns}
|
||||
items={items}
|
||||
minWidth="1320px"
|
||||
rowKey={(room) => room.roomId}
|
||||
pagination={
|
||||
total > 0
|
||||
? {
|
||||
page: page.page,
|
||||
pageSize: page.data.pageSize || 50,
|
||||
total,
|
||||
onPageChange: page.setPage,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</DataState>
|
||||
</div>
|
||||
|
||||
<AdminFormDialog
|
||||
loading={page.loadingAction === "create"}
|
||||
open={page.activeAction === "create"}
|
||||
size="wide"
|
||||
submitDisabled={!page.abilities.canRobotCreate || page.loadingAction === "create"}
|
||||
title="增加机器人房间"
|
||||
onClose={page.closeAction}
|
||||
onSubmit={page.submitCreate}
|
||||
>
|
||||
<TextField
|
||||
label="房间名称"
|
||||
value={page.form.roomName}
|
||||
onChange={(event) => page.setForm({ ...page.form, roomName: event.target.value })}
|
||||
/>
|
||||
<Autocomplete
|
||||
getOptionLabel={robotOptionLabel}
|
||||
isOptionEqualToValue={(option, value) => option.userId === value.userId}
|
||||
loading={page.availableRobotsLoading}
|
||||
noOptionsText="当前无数据"
|
||||
options={page.availableRobots}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={Boolean(page.availableRobotsError)}
|
||||
helperText={page.availableRobotsError}
|
||||
label="房主机器人"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} key={option.userId}>
|
||||
<RobotIdentity user={option.user} fallbackId={option.userId} />
|
||||
</li>
|
||||
)}
|
||||
size="small"
|
||||
value={page.selectedOwnerRobot}
|
||||
onChange={(_, option) => page.selectOwnerRobot(option)}
|
||||
/>
|
||||
<Autocomplete
|
||||
multiple
|
||||
disableCloseOnSelect
|
||||
getOptionLabel={robotOptionLabel}
|
||||
isOptionEqualToValue={(option, value) => option.userId === value.userId}
|
||||
loading={page.availableRobotsLoading}
|
||||
noOptionsText="当前无数据"
|
||||
options={page.availableRobots}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={Boolean(page.availableRobotsError)}
|
||||
helperText={page.availableRobotsError}
|
||||
label="候选机器人"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} key={option.userId}>
|
||||
<RobotIdentity user={option.user} fallbackId={option.userId} />
|
||||
</li>
|
||||
)}
|
||||
size="small"
|
||||
value={page.selectedCandidateRobots}
|
||||
onChange={(_, options) => page.selectCandidateRobots(options)}
|
||||
/>
|
||||
<div className={styles.formGrid}>
|
||||
<TextField
|
||||
label="最少进房人数"
|
||||
required
|
||||
type="number"
|
||||
value={page.form.minRobotCount}
|
||||
onChange={(event) => page.setForm({ ...page.form, minRobotCount: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="最多进房人数"
|
||||
required
|
||||
type="number"
|
||||
value={page.form.maxRobotCount}
|
||||
onChange={(event) => page.setForm({ ...page.form, maxRobotCount: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<Autocomplete
|
||||
multiple
|
||||
disableCloseOnSelect
|
||||
getOptionLabel={giftOptionLabel}
|
||||
isOptionEqualToValue={(option, value) => giftId(option) === giftId(value)}
|
||||
loading={page.giftOptionsLoading}
|
||||
noOptionsText="当前无数据"
|
||||
options={page.giftOptions}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={Boolean(page.giftOptionsError)}
|
||||
helperText={page.giftOptionsError}
|
||||
label="礼物合集"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
size="small"
|
||||
value={page.selectedGiftOptions}
|
||||
onChange={(_, options) => page.selectGiftIds("giftIds", options)}
|
||||
/>
|
||||
<TextField
|
||||
label="普通礼物频次(毫秒)"
|
||||
required
|
||||
type="number"
|
||||
value={page.form.normalGiftIntervalMs}
|
||||
onChange={(event) => page.setForm({ ...page.form, normalGiftIntervalMs: event.target.value })}
|
||||
/>
|
||||
<Autocomplete
|
||||
multiple
|
||||
disableCloseOnSelect
|
||||
getOptionLabel={giftOptionLabel}
|
||||
isOptionEqualToValue={(option, value) => giftId(option) === giftId(value)}
|
||||
loading={page.giftOptionsLoading}
|
||||
noOptionsText="当前无数据"
|
||||
options={page.giftOptions}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={Boolean(page.giftOptionsError)}
|
||||
helperText={page.giftOptionsError}
|
||||
label="幸运礼物合集"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
size="small"
|
||||
value={page.selectedLuckyGiftOptions}
|
||||
onChange={(_, options) => page.selectGiftIds("luckyGiftIds", options)}
|
||||
/>
|
||||
<div className={styles.formGrid}>
|
||||
<TextField
|
||||
label="最少幸运连击"
|
||||
required
|
||||
type="number"
|
||||
value={page.form.luckyComboMin}
|
||||
onChange={(event) => page.setForm({ ...page.form, luckyComboMin: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="最多幸运连击"
|
||||
required
|
||||
type="number"
|
||||
value={page.form.luckyComboMax}
|
||||
onChange={(event) => page.setForm({ ...page.form, luckyComboMax: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="最短幸运间隔(毫秒)"
|
||||
required
|
||||
type="number"
|
||||
value={page.form.luckyPauseMinMs}
|
||||
onChange={(event) => page.setForm({ ...page.form, luckyPauseMinMs: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="最长幸运间隔(毫秒)"
|
||||
required
|
||||
type="number"
|
||||
value={page.form.luckyPauseMaxMs}
|
||||
onChange={(event) => page.setForm({ ...page.form, luckyPauseMaxMs: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</AdminFormDialog>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function RobotRoomIdentity({ room }) {
|
||||
return (
|
||||
<div className={styles.identity}>
|
||||
<span className={styles.cover}>
|
||||
{room.coverUrl ? <img src={room.coverUrl} alt="" /> : <BedroomParentOutlined fontSize="small" />}
|
||||
</span>
|
||||
<div className={styles.identityText}>
|
||||
<div className={styles.name}>{room.title || "-"}</div>
|
||||
<div className={styles.meta}>长ID {room.roomId || "-"}</div>
|
||||
<div className={styles.meta}>短ID {room.roomShortId || "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RobotIdentity({ user = {}, fallbackId = "" }) {
|
||||
return <AdminUserIdentity openInAppUserDetail rows={[user.displayUserId, user.userId || fallbackId]} user={user} />;
|
||||
}
|
||||
|
||||
function RobotRoomStatus({ page, room }) {
|
||||
const checked = room.status === "active";
|
||||
return (
|
||||
<div className={styles.statusCell}>
|
||||
<Tooltip arrow title={checked ? "停止" : "启动"}>
|
||||
<span>
|
||||
<AdminSwitch
|
||||
checked={checked}
|
||||
checkedLabel="运行"
|
||||
disabled={!page.abilities.canRobotUpdate || page.loadingAction === `status-${room.roomId}`}
|
||||
label={checked ? "停止机器人房间" : "启动机器人房间"}
|
||||
uncheckedLabel="停止"
|
||||
onChange={() => page.setRoomStatus(room)}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RuleStack({ rows }) {
|
||||
return (
|
||||
<div className={styles.stack}>
|
||||
{rows.map((row) => (
|
||||
<span className={styles.meta} key={row}>
|
||||
{row}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatSeconds(ms) {
|
||||
const seconds = Math.round(Number(ms || 0) / 1000);
|
||||
return `${seconds.toLocaleString("zh-CN")}秒`;
|
||||
}
|
||||
@ -11,6 +11,9 @@ export function useRoomAbilities() {
|
||||
canPinCancel: can(PERMISSIONS.roomPinCancel),
|
||||
canPinCreate: can(PERMISSIONS.roomPinCreate),
|
||||
canPinView: can(PERMISSIONS.roomPinView),
|
||||
canRobotCreate: can(PERMISSIONS.roomRobotCreate),
|
||||
canRobotUpdate: can(PERMISSIONS.roomRobotUpdate),
|
||||
canRobotView: can(PERMISSIONS.roomRobotView),
|
||||
canUpdate: can(PERMISSIONS.roomUpdate),
|
||||
canUpload: can(PERMISSIONS.uploadCreate),
|
||||
canView: can(PERMISSIONS.roomView),
|
||||
|
||||
@ -54,6 +54,18 @@
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.formGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.formGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
flex: 0 1 320px;
|
||||
}
|
||||
|
||||
@ -25,4 +25,12 @@ export const roomRoutes = [
|
||||
path: "/rooms/config",
|
||||
permission: PERMISSIONS.roomConfigView,
|
||||
},
|
||||
{
|
||||
label: "机器人房间",
|
||||
loader: () => import("./pages/RoomRobotPage.jsx").then((module) => module.RoomRobotPage),
|
||||
menuCode: MENU_CODES.roomRobots,
|
||||
pageKey: "room-robots",
|
||||
path: "/rooms/robots",
|
||||
permission: PERMISSIONS.roomRobotView,
|
||||
},
|
||||
];
|
||||
|
||||
@ -70,7 +70,37 @@ export const roomPinCreateSchema = z
|
||||
path: ["expiresAtMs"],
|
||||
});
|
||||
|
||||
export const robotRoomCreateSchema = z
|
||||
.object({
|
||||
candidateRobotUserIds: z.array(z.coerce.number().int().positive()).min(1, "请选择候选机器人"),
|
||||
giftIds: z.array(z.string().trim().min(1)).min(1, "请选择普通礼物合集"),
|
||||
luckyComboMax: z.coerce.number().int().min(1, "幸运礼物连击范围不正确"),
|
||||
luckyComboMin: z.coerce.number().int().min(1, "幸运礼物连击范围不正确"),
|
||||
luckyGiftIds: z.array(z.string().trim().min(1)).min(1, "请选择幸运礼物合集"),
|
||||
luckyPauseMaxMs: z.coerce.number().int().min(1000, "幸运礼物间隔范围不正确"),
|
||||
luckyPauseMinMs: z.coerce.number().int().min(1000, "幸运礼物间隔范围不正确"),
|
||||
maxRobotCount: z.coerce.number().int().min(1, "机器人数量范围不正确"),
|
||||
minRobotCount: z.coerce.number().int().min(1, "机器人数量范围不正确"),
|
||||
normalGiftIntervalMs: z.coerce.number().int().min(1000, "普通礼物频次不正确"),
|
||||
ownerRobotUserId: z.coerce.number().int().positive("请选择房主机器人"),
|
||||
roomName: z.string().trim().max(128, "房间名称不能超过 128 个字符").optional().default(""),
|
||||
visibleRegionId: z.coerce.number().int().min(0, "区域 ID 不正确").default(0),
|
||||
})
|
||||
.refine((value) => value.maxRobotCount >= value.minRobotCount, {
|
||||
message: "机器人数量范围不正确",
|
||||
path: ["maxRobotCount"],
|
||||
})
|
||||
.refine((value) => value.luckyComboMax >= value.luckyComboMin, {
|
||||
message: "幸运礼物连击范围不正确",
|
||||
path: ["luckyComboMax"],
|
||||
})
|
||||
.refine((value) => value.luckyPauseMaxMs >= value.luckyPauseMinMs, {
|
||||
message: "幸运礼物间隔范围不正确",
|
||||
path: ["luckyPauseMaxMs"],
|
||||
});
|
||||
|
||||
export type RoomUpdateForm = z.infer<typeof roomUpdateSchema>;
|
||||
export type RoomStatusForm = z.infer<typeof roomStatusSchema>;
|
||||
export type RoomConfigForm = z.infer<typeof roomConfigSchema>;
|
||||
export type RoomPinCreateForm = z.infer<typeof roomPinCreateSchema>;
|
||||
export type RobotRoomCreateForm = z.infer<typeof robotRoomCreateSchema>;
|
||||
|
||||
@ -52,6 +52,7 @@ export const API_OPERATIONS = {
|
||||
createRegion: "createRegion",
|
||||
createResource: "createResource",
|
||||
createResourceGroup: "createResourceGroup",
|
||||
createRobotRoom: "createRobotRoom",
|
||||
createRole: "createRole",
|
||||
createRoomPin: "createRoomPin",
|
||||
createSplashScreen: "createSplashScreen",
|
||||
@ -128,6 +129,7 @@ export const API_OPERATIONS = {
|
||||
listAgencies: "listAgencies",
|
||||
listApps: "listApps",
|
||||
listAppVersions: "listAppVersions",
|
||||
listAvailableRoomRobots: "listAvailableRoomRobots",
|
||||
listBanners: "listBanners",
|
||||
listBDLeaders: "listBDLeaders",
|
||||
listBDs: "listBDs",
|
||||
@ -172,6 +174,7 @@ export const API_OPERATIONS = {
|
||||
listResourceGroups: "listResourceGroups",
|
||||
listResources: "listResources",
|
||||
listResourceShopItems: "listResourceShopItems",
|
||||
listRobotRooms: "listRobotRooms",
|
||||
listRoles: "listRoles",
|
||||
listRoomPins: "listRoomPins",
|
||||
listRoomRpsChallenges: "listRoomRpsChallenges",
|
||||
@ -218,6 +221,8 @@ export const API_OPERATIONS = {
|
||||
setThirdPartyPaymentMethodStatus: "setThirdPartyPaymentMethodStatus",
|
||||
setWeeklyStarCycleStatus: "setWeeklyStarCycleStatus",
|
||||
sortMenus: "sortMenus",
|
||||
startRobotRoom: "startRobotRoom",
|
||||
stopRobotRoom: "stopRobotRoom",
|
||||
syncPermissions: "syncPermissions",
|
||||
syncThirdPartyPaymentRates: "syncThirdPartyPaymentRates",
|
||||
updateAchievementDefinition: "updateAchievementDefinition",
|
||||
@ -546,6 +551,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "resource-group:create",
|
||||
permissions: ["resource-group:create"]
|
||||
},
|
||||
createRobotRoom: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.createRobotRoom,
|
||||
path: "/v1/admin/rooms/robot-rooms",
|
||||
permission: "room-robot:create",
|
||||
permissions: ["room-robot:create"]
|
||||
},
|
||||
createRole: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.createRole,
|
||||
@ -1076,6 +1088,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "app-version:view",
|
||||
permissions: ["app-version:view"]
|
||||
},
|
||||
listAvailableRoomRobots: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listAvailableRoomRobots,
|
||||
path: "/v1/admin/rooms/robot-rooms/available-robots",
|
||||
permission: "room-robot:view",
|
||||
permissions: ["room-robot:view"]
|
||||
},
|
||||
listBanners: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listBanners,
|
||||
@ -1384,6 +1403,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "resource-shop:view",
|
||||
permissions: ["resource-shop:view"]
|
||||
},
|
||||
listRobotRooms: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listRobotRooms,
|
||||
path: "/v1/admin/rooms/robot-rooms",
|
||||
permission: "room-robot:view",
|
||||
permissions: ["room-robot:view"]
|
||||
},
|
||||
listRoles: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listRoles,
|
||||
@ -1694,6 +1720,20 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "menu:sort",
|
||||
permissions: ["menu:sort"]
|
||||
},
|
||||
startRobotRoom: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.startRobotRoom,
|
||||
path: "/v1/admin/rooms/robot-rooms/{room_id}/start",
|
||||
permission: "room-robot:update",
|
||||
permissions: ["room-robot:update"]
|
||||
},
|
||||
stopRobotRoom: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.stopRobotRoom,
|
||||
path: "/v1/admin/rooms/robot-rooms/{room_id}/stop",
|
||||
permission: "room-robot:update",
|
||||
permissions: ["room-robot:update"]
|
||||
},
|
||||
syncPermissions: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.syncPermissions,
|
||||
|
||||
128
src/shared/api/generated/schema.d.ts
vendored
128
src/shared/api/generated/schema.d.ts
vendored
@ -2164,6 +2164,70 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/rooms/robot-rooms": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listRobotRooms"];
|
||||
put?: never;
|
||||
post: operations["createRobotRoom"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/rooms/robot-rooms/available-robots": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listAvailableRoomRobots"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/rooms/robot-rooms/{room_id}/start": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["startRobotRoom"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/rooms/robot-rooms/{room_id}/stop": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["stopRobotRoom"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/users/level-config": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -5977,6 +6041,70 @@ export interface operations {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
listRobotRooms: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
createRobotRoom: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
listAvailableRoomRobots: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
startRobotRoom: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
room_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
stopRobotRoom: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
room_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
listLevelConfig: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@ -938,6 +938,61 @@ export interface RoomPinPayload {
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export interface RobotGiftRuleDto {
|
||||
giftIds?: string[];
|
||||
luckyGiftIds?: string[];
|
||||
luckyComboMax?: number;
|
||||
luckyComboMin?: number;
|
||||
luckyPauseMaxMs?: number;
|
||||
luckyPauseMinMs?: number;
|
||||
normalGiftIntervalMs?: number;
|
||||
}
|
||||
|
||||
export interface RobotRoomDto {
|
||||
appCode?: string;
|
||||
coverUrl?: string;
|
||||
createdAtMs?: number;
|
||||
createdByAdminId?: number;
|
||||
giftRule?: RobotGiftRuleDto;
|
||||
owner?: RoomOwnerDto;
|
||||
ownerRobotUserId?: string;
|
||||
regionName?: string;
|
||||
robots?: RoomOwnerDto[];
|
||||
robotUserIds?: string[];
|
||||
roomId: string;
|
||||
roomShortId?: string;
|
||||
status?: string;
|
||||
title?: string;
|
||||
updatedAtMs?: number;
|
||||
visibleRegionId?: number;
|
||||
}
|
||||
|
||||
export interface AvailableRoomRobotDto {
|
||||
lastUsedAtMs?: number;
|
||||
roomScene?: string;
|
||||
status?: string;
|
||||
usedCount?: number;
|
||||
user?: RoomOwnerDto;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface RobotRoomPayload {
|
||||
candidateRobotUserIds: EntityId[];
|
||||
giftIds: string[];
|
||||
luckyComboMax: number;
|
||||
luckyComboMin: number;
|
||||
luckyGiftIds: string[];
|
||||
luckyPauseMaxMs: number;
|
||||
luckyPauseMinMs: number;
|
||||
maxRobotCount: number;
|
||||
minRobotCount: number;
|
||||
normalGiftIntervalMs: number;
|
||||
ownerRobotUserId: EntityId;
|
||||
roomAvatar?: string;
|
||||
roomName?: string;
|
||||
visibleRegionId?: EntityId;
|
||||
}
|
||||
|
||||
export interface RoomOwnerDto {
|
||||
avatar?: string;
|
||||
displayUserId?: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user