2026-06-16 20:47:19 +08:00

335 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import AddOutlined from "@mui/icons-material/AddOutlined";
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
import Avatar from "@mui/material/Avatar";
import MenuItem from "@mui/material/MenuItem";
import Switch from "@mui/material/Switch";
import TextField from "@mui/material/TextField";
import { useCallback, useEffect, useMemo, useState } from "react";
import { PERMISSIONS } from "@/app/permissions";
import { useAuth } from "@/app/auth/AuthProvider.jsx";
import { deleteDiceRobot, generateDiceRobots, listDiceRobots, setDiceRobotStatus } from "@/features/games/api";
import { useCountryOptions } from "@/features/host-org/hooks/useCountryOptions.js";
import styles from "@/features/games/games.module.css";
import {
AdminActionIconButton,
AdminFilterResetButton,
AdminFilterSelect,
AdminListBody,
AdminListPage,
AdminListToolbar,
AdminRowActions,
} from "@/shared/ui/AdminListLayout.jsx";
import { AdminFormDialog, AdminFormFieldGrid } from "@/shared/ui/AdminFormDialog.jsx";
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { PageSkeleton } from "@/shared/ui/PageSkeleton.jsx";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
import { formatMillis } from "@/shared/utils/time.js";
const defaultGenerateForm = {
count: 20,
country: "SA",
nicknameLanguage: "arabic",
};
const maxGenerateRobotCount = 1000;
export function GameRobotPage() {
const { can } = useAuth();
const confirm = useConfirm();
const { showToast } = useToast();
const canCreate = can(PERMISSIONS.gameCreate);
const canUpdate = can(PERMISSIONS.gameUpdate);
const canDelete = can(PERMISSIONS.gameDelete) || canUpdate;
const { countryOptions, loadingCountries } = useCountryOptions();
const [items, setItems] = useState([]);
const [status, setStatus] = useState("");
const [nextCursor, setNextCursor] = useState("");
const [loading, setLoading] = useState(true);
const [loadingAction, setLoadingAction] = useState("");
const [dialogOpen, setDialogOpen] = useState(false);
const [generateForm, setGenerateForm] = useState(defaultGenerateForm);
const load = useCallback(
async (cursor = "", append = false) => {
setLoading(!append);
try {
const result = await listDiceRobots({ status, cursor, pageSize: 50 });
setItems((current) => (append ? [...current, ...(result.items || [])] : result.items || []));
setNextCursor(result.nextCursor || "");
} catch (err) {
showToast(err.message || "加载全站机器人失败", "error");
} finally {
setLoading(false);
}
},
[showToast, status],
);
useEffect(() => {
load("", false);
}, [load]);
useEffect(() => {
setGenerateForm((current) => {
if (countryOptions.length === 0) {
return current;
}
const currentCountryEnabled = countryOptions.some((option) => option.value === current.country);
if (current.country && currentCountryEnabled) {
return current;
}
// 国家主数据由后端控制启用状态;默认优先沿用历史 SA否则取当前启用列表第一项避免提交空国家被 user-service 拒绝。
const defaultCountry = countryOptions.find((option) => option.value === "SA")?.value || countryOptions[0].value;
return { ...current, country: defaultCountry };
});
}, [countryOptions]);
const changeStatus = useCallback(
async (robot, checked) => {
setLoadingAction(`status-${robot.userId}`);
try {
await setDiceRobotStatus(robot.userId, checked ? "active" : "disabled");
await load("", false);
showToast("机器人状态已更新", "success");
} catch (err) {
showToast(err.message || "更新机器人状态失败", "error");
} finally {
setLoadingAction("");
}
},
[load, showToast],
);
const removeRobot = useCallback(
async (robot) => {
const ok = await confirm({
confirmText: "删除",
message: `${robot.nickname || robot.userId} 会从全站机器人列表移除,历史用户资料和对局记录保留。`,
title: "删除全站机器人",
tone: "danger",
});
if (!ok) {
return;
}
setLoadingAction(`delete-${robot.userId}`);
try {
await deleteDiceRobot(robot.userId);
await load("", false);
showToast("全站机器人已删除", "success");
} catch (err) {
showToast(err.message || "删除全站机器人失败", "error");
} finally {
setLoadingAction("");
}
},
[confirm, load, showToast],
);
const columns = useMemo(
() => [
{
key: "user",
label: "机器人用户",
width: "minmax(220px, 1fr)",
render: (robot) => (
<div className={styles.identity}>
<Avatar className={styles.robotAvatar} src={robot.avatar || undefined}>
{robotAvatarText(robot)}
</Avatar>
<div className={styles.identityText} title={`UID ${robot.userId}`}>
<span className={styles.name}>{robot.nickname || robot.userId}</span>
<span className={styles.meta}>{robot.shortId ? `短ID ${robot.shortId}` : "短ID -"}</span>
</div>
</div>
),
},
{
key: "status",
label: "状态",
width: "120px",
render: (robot) => (
<Switch
checked={robot.status === "active"}
disabled={!canUpdate || loadingAction === `status-${robot.userId}`}
inputProps={{ "aria-label": `${robot.userId} 状态` }}
onChange={(event) => changeStatus(robot, event.target.checked)}
/>
),
},
{
key: "created",
label: "创建时间",
width: "minmax(170px, 1fr)",
render: (robot) => (robot.createdAtMs ? formatMillis(robot.createdAtMs) : "-"),
},
{
key: "updated",
label: "更新时间",
width: "minmax(170px, 1fr)",
render: (robot) => (robot.updatedAtMs ? formatMillis(robot.updatedAtMs) : "-"),
},
{
key: "actions",
label: "操作",
width: "96px",
render: (robot) => (
<AdminRowActions>
<AdminActionIconButton
disabled={
!canDelete || loadingAction === `delete-${robot.userId}` || Boolean(loadingAction)
}
label="删除"
tone="danger"
onClick={() => removeRobot(robot)}
>
<DeleteOutlineOutlined fontSize="small" />
</AdminActionIconButton>
</AdminRowActions>
),
},
],
[canDelete, canUpdate, changeStatus, loadingAction, removeRobot],
);
const submitGenerate = async (event) => {
event.preventDefault();
setLoadingAction("generate");
try {
const enabledCountry = countryOptions.some((option) => option.value === generateForm.country)
? generateForm.country
: countryOptions[0]?.value || generateForm.country || "SA";
const result = await generateDiceRobots({
country: enabledCountry,
count: Number(generateForm.count || 0),
nicknameLanguage: generateForm.nicknameLanguage || "arabic",
});
await load("", false);
showToast(`已批量创建 ${result.created || 0} 个机器人`, "success");
setDialogOpen(false);
} catch (err) {
showToast(err.message || "批量创建全站机器人失败", "error");
} finally {
setLoadingAction("");
}
};
if (loading) {
return <PageSkeleton />;
}
return (
<AdminListPage>
<AdminListToolbar
actions={
<>
<AdminActionIconButton label="刷新" onClick={() => load("", false)}>
<RefreshOutlined fontSize="small" />
</AdminActionIconButton>
<AdminActionIconButton
disabled={!canCreate}
label="批量创建"
primary
onClick={() => setDialogOpen(true)}
>
<AddOutlined fontSize="small" />
</AdminActionIconButton>
</>
}
filters={
<>
<AdminFilterSelect
label="状态"
value={status}
onChange={(event) => setStatus(event.target.value)}
>
<MenuItem value="">全部状态</MenuItem>
<MenuItem value="active">启用</MenuItem>
<MenuItem value="disabled">停用</MenuItem>
</AdminFilterSelect>
<AdminFilterResetButton disabled={!status} onClick={() => setStatus("")} />
</>
}
/>
<AdminListBody>
<DataTable
columns={columns}
infiniteScroll={{
hasMore: Boolean(nextCursor),
loading: false,
onLoadMore: () => load(nextCursor, true),
}}
items={items}
minWidth="760px"
rowKey={(robot) => robot.userId}
/>
</AdminListBody>
<GenerateDialog
form={generateForm}
countryOptions={countryOptions}
loadingCountries={loadingCountries}
loading={loadingAction === "generate"}
open={dialogOpen}
setForm={setGenerateForm}
onClose={() => setDialogOpen(false)}
onSubmit={submitGenerate}
/>
</AdminListPage>
);
}
function GenerateDialog({ countryOptions, form, loading, loadingCountries, open, setForm, onClose, onSubmit }) {
const selectCountryOptions = countryOptions.length > 0 ? countryOptions : [{ label: "SA", value: "SA" }];
const selectedCountry = selectCountryOptions.some((country) => country.value === form.country)
? form.country
: selectCountryOptions[0]?.value || "SA";
return (
<AdminFormDialog
loading={loading}
open={open}
submitLabel="创建"
title="批量创建全站机器人"
onClose={onClose}
onSubmit={onSubmit}
>
<AdminFormFieldGrid>
<TextField
helperText={`最多 ${maxGenerateRobotCount}`}
label="数量"
type="number"
value={form.count}
slotProps={{ htmlInput: { max: maxGenerateRobotCount, min: 1 } }}
onChange={(event) => setForm({ ...form, count: event.target.value })}
/>
<TextField
label="账号语言"
select
value={form.nicknameLanguage || "arabic"}
onChange={(event) => setForm({ ...form, nicknameLanguage: event.target.value })}
>
<MenuItem value="arabic">阿拉伯语账号</MenuItem>
<MenuItem value="english">英语账号</MenuItem>
</TextField>
<TextField
disabled={loadingCountries}
label="国家"
select
value={selectedCountry}
onChange={(event) => setForm({ ...form, country: event.target.value })}
>
{selectCountryOptions.map((country) => (
<MenuItem key={country.value} value={country.value}>
{country.label}
</MenuItem>
))}
</TextField>
</AdminFormFieldGrid>
</AdminFormDialog>
);
}
function robotAvatarText(robot) {
const value = String(robot.nickname || robot.shortId || robot.userId || "?").trim();
return value ? value.slice(0, 1).toUpperCase() : "?";
}