2026-06-17 23:13:03 +08:00

725 lines
30 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 BedroomParentOutlined from "@mui/icons-material/BedroomParentOutlined";
import TuneOutlined from "@mui/icons-material/TuneOutlined";
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,
robotLocationLabel,
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(150px, 0.6fr)",
render: (room) => (
<RuleStack
rows={[
`活跃 ${Number(room.activeRobotCount || 0).toLocaleString("zh-CN")}`,
`候选 ${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: "behaviorRule",
label: "行为",
width: "minmax(220px, 0.9fr)",
render: (room) => (
<RuleStack
rows={[
`${formatDurationRange(room.giftRule?.robotStayMinMs, room.giftRule?.robotStayMaxMs)} 停留`,
`${formatDurationRange(room.giftRule?.robotReplaceMinMs, room.giftRule?.robotReplaceMaxMs)} 补位`,
`最多 ${Number(room.giftRule?.maxGiftSenders || 0).toLocaleString("zh-CN")} 个送礼`,
]}
/>
),
},
{
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={<TuneOutlined fontSize="small" />}
variant="secondary"
onClick={page.openHumanConfig}
>
真人房间机器人配置
</Button>
<Button
startIcon={<AddOutlined fontSize="small" />}
variant="primary"
onClick={page.openCreate}
>
增加机器人房间
</Button>
</div>
) : null}
</div>
<HumanConfigPanel page={page} />
<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}
>
<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}>
<OwnerRobotOption
countryLabels={page.robotCountryLabels}
loadingLocation={page.loadingRobotLocationOptions}
option={option}
regionLabels={page.robotRegionLabels}
/>
</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.candidateRobotOptions}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(page.availableRobotsError)}
helperText={page.availableRobotsError}
label={<RequiredLabel>候选机器人</RequiredLabel>}
/>
)}
renderOption={(props, option) => (
<li {...props} key={option.userId}>
<RobotIdentity openInAppUserDetail={false} 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={<RequiredLabel>礼物合集</RequiredLabel>}
/>
)}
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 })}
/>
<div className={styles.formGrid}>
<TextField
label="最短停留(毫秒)"
required
type="number"
value={page.form.robotStayMinMs}
onChange={(event) => page.setForm({ ...page.form, robotStayMinMs: event.target.value })}
/>
<TextField
label="最长停留(毫秒)"
required
type="number"
value={page.form.robotStayMaxMs}
onChange={(event) => page.setForm({ ...page.form, robotStayMaxMs: event.target.value })}
/>
<TextField
label="最短补位(毫秒)"
required
type="number"
value={page.form.robotReplaceMinMs}
onChange={(event) => page.setForm({ ...page.form, robotReplaceMinMs: event.target.value })}
/>
<TextField
label="最长补位(毫秒)"
required
type="number"
value={page.form.robotReplaceMaxMs}
onChange={(event) => page.setForm({ ...page.form, robotReplaceMaxMs: event.target.value })}
/>
<TextField
label="同时送礼上限"
required
type="number"
value={page.form.maxGiftSenders}
onChange={(event) => page.setForm({ ...page.form, maxGiftSenders: 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={<RequiredLabel>幸运礼物合集</RequiredLabel>}
/>
)}
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>
<HumanConfigDialog page={page} />
</section>
);
}
function HumanConfigPanel({ page }) {
const config = page.humanConfig || {};
return (
<section className={styles.robotConfigPanel}>
<div>
<div className={styles.primaryText}>真人房间机器人</div>
<div className={styles.meta}>
{page.humanConfigLoading
? "配置加载中"
: page.humanConfigError || (config.enabled ? "已启用" : "未启用")}
</div>
</div>
<RuleStack
rows={[
`低于 ${Number(config.candidateRoomMaxOnline || 5).toLocaleString("zh-CN")} 人进房`,
`目标 ${Number(config.roomTargetMinOnline || config.roomFullStopOnline || 8).toLocaleString("zh-CN")}-${Number(config.roomTargetMaxOnline || config.roomFullStopOnline || 10).toLocaleString("zh-CN")}`,
`${formatDurationRange(config.robotStayMinMs || 60000, config.robotStayMaxMs || 600000)} 停留`,
]}
/>
<RuleStack
rows={[
"自动使用全站机器人",
`${formatDurationRange(
config.normalGiftIntervalMinMs || config.normalGiftIntervalMs || 10000,
config.normalGiftIntervalMaxMs || config.normalGiftIntervalMs || 10000,
)} / 次`,
`${Number(config.maxGiftSenders || 1).toLocaleString("zh-CN")} 个机器人送礼`,
`${config.giftIds?.length || 0} 个普通礼物`,
`${(config.luckyGiftIds?.length || 0) + (config.superLuckyGiftIds?.length || 0)} 个幸运礼物`,
]}
/>
{page.abilities.canRobotUpdate ? (
<Button
loading={page.humanConfigLoading}
startIcon={<TuneOutlined fontSize="small" />}
variant="secondary"
onClick={page.openHumanConfig}
>
配置
</Button>
) : null}
</section>
);
}
function HumanConfigDialog({ page }) {
return (
<AdminFormDialog
loading={page.loadingAction === "human-config"}
open={page.activeAction === "human-config"}
size="wide"
submitDisabled={!page.abilities.canRobotUpdate || page.loadingAction === "human-config"}
title="真人房间机器人配置"
onClose={page.closeAction}
onSubmit={page.submitHumanConfig}
>
<div className={styles.formGrid}>
<label className={styles.switchField}>
<AdminSwitch
checked={Boolean(page.humanConfigForm.enabled)}
checkedLabel="启用"
label="启用真人房间机器人"
uncheckedLabel="关闭"
onChange={(_, checked) =>
page.setHumanConfigForm({ ...page.humanConfigForm, enabled: checked })
}
/>
</label>
<TextField
label="真人房间低于人数"
required
type="number"
value={page.humanConfigForm.candidateRoomMaxOnline}
onChange={(event) =>
page.setHumanConfigForm({
...page.humanConfigForm,
candidateRoomMaxOnline: event.target.value,
})
}
/>
<TextField
label="目标人数下限"
required
type="number"
value={page.humanConfigForm.roomTargetMinOnline}
onChange={(event) =>
page.setHumanConfigForm({ ...page.humanConfigForm, roomTargetMinOnline: event.target.value })
}
/>
<TextField
label="目标人数上限"
required
type="number"
value={page.humanConfigForm.roomTargetMaxOnline}
onChange={(event) =>
page.setHumanConfigForm({ ...page.humanConfigForm, roomTargetMaxOnline: event.target.value })
}
/>
</div>
<div className={styles.formGrid}>
<TextField
label="最短送礼间隔(毫秒)"
required
type="number"
value={page.humanConfigForm.normalGiftIntervalMinMs}
onChange={(event) =>
page.setHumanConfigForm({
...page.humanConfigForm,
normalGiftIntervalMinMs: event.target.value,
})
}
/>
<TextField
label="最长送礼间隔(毫秒)"
required
type="number"
value={page.humanConfigForm.normalGiftIntervalMaxMs}
onChange={(event) =>
page.setHumanConfigForm({
...page.humanConfigForm,
normalGiftIntervalMaxMs: event.target.value,
})
}
/>
<TextField
label="最短停留(毫秒)"
required
type="number"
value={page.humanConfigForm.robotStayMinMs}
onChange={(event) =>
page.setHumanConfigForm({ ...page.humanConfigForm, robotStayMinMs: event.target.value })
}
/>
<TextField
label="最长停留(毫秒)"
required
type="number"
value={page.humanConfigForm.robotStayMaxMs}
onChange={(event) =>
page.setHumanConfigForm({ ...page.humanConfigForm, robotStayMaxMs: event.target.value })
}
/>
<TextField
label="最短补位(毫秒)"
required
type="number"
value={page.humanConfigForm.robotReplaceMinMs}
onChange={(event) =>
page.setHumanConfigForm({ ...page.humanConfigForm, robotReplaceMinMs: event.target.value })
}
/>
<TextField
label="最长补位(毫秒)"
required
type="number"
value={page.humanConfigForm.robotReplaceMaxMs}
onChange={(event) =>
page.setHumanConfigForm({ ...page.humanConfigForm, robotReplaceMaxMs: event.target.value })
}
/>
<TextField
label="房间送礼机器人数"
required
type="number"
value={page.humanConfigForm.maxGiftSenders}
onChange={(event) =>
page.setHumanConfigForm({ ...page.humanConfigForm, maxGiftSenders: event.target.value })
}
/>
</div>
<GiftAutocomplete
label="普通礼物合集"
loading={page.giftOptionsLoading}
options={page.giftOptions}
required
value={page.selectedHumanGiftOptions.giftIds}
onChange={(options) => page.selectHumanGiftIds("giftIds", options)}
/>
<div className={styles.formGrid}>
<TextField
label="最少幸运连击"
required
type="number"
value={page.humanConfigForm.luckyComboMin}
onChange={(event) =>
page.setHumanConfigForm({ ...page.humanConfigForm, luckyComboMin: event.target.value })
}
/>
<TextField
label="最多幸运连击"
required
type="number"
value={page.humanConfigForm.luckyComboMax}
onChange={(event) =>
page.setHumanConfigForm({ ...page.humanConfigForm, luckyComboMax: event.target.value })
}
/>
<TextField
label="最短幸运间隔(毫秒)"
required
type="number"
value={page.humanConfigForm.luckyPauseMinMs}
onChange={(event) =>
page.setHumanConfigForm({ ...page.humanConfigForm, luckyPauseMinMs: event.target.value })
}
/>
<TextField
label="最长幸运间隔(毫秒)"
required
type="number"
value={page.humanConfigForm.luckyPauseMaxMs}
onChange={(event) =>
page.setHumanConfigForm({ ...page.humanConfigForm, luckyPauseMaxMs: event.target.value })
}
/>
</div>
<GiftAutocomplete
label="幸运礼物合集"
loading={page.giftOptionsLoading}
options={page.giftOptions}
value={page.selectedHumanGiftOptions.luckyGiftIds}
onChange={(options) => page.selectHumanGiftIds("luckyGiftIds", options)}
/>
<GiftAutocomplete
label="超级幸运礼物合集"
loading={page.giftOptionsLoading}
options={page.giftOptions}
value={page.selectedHumanGiftOptions.superLuckyGiftIds}
onChange={(options) => page.selectHumanGiftIds("superLuckyGiftIds", options)}
/>
<section className={styles.robotAutoPoolNotice}>
<div className={styles.primaryText}>机器人池自动获取</div>
<div className={styles.meta}>
系统会读取所有启用的全站机器人并按机器人账号国家进入同国家真人房间
</div>
</section>
</AdminFormDialog>
);
}
function GiftAutocomplete({ label, loading, onChange, options, required = false, value }) {
return (
<Autocomplete
multiple
disableCloseOnSelect
getOptionLabel={giftOptionLabel}
isOptionEqualToValue={(option, selected) => giftId(option) === giftId(selected)}
loading={loading}
noOptionsText="当前无数据"
options={options}
renderInput={(params) => (
<TextField {...params} label={required ? <RequiredLabel>{label}</RequiredLabel> : label} />
)}
size="small"
value={value}
onChange={(_, selected) => onChange(selected)}
/>
);
}
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({ fallbackId = "", openInAppUserDetail = true, user = {} }) {
return (
<AdminUserIdentity
openInAppUserDetail={openInAppUserDetail}
rows={[user.displayUserId, user.userId || fallbackId]}
user={user}
/>
);
}
function OwnerRobotOption({ countryLabels, loadingLocation, option, regionLabels }) {
return (
<div className={styles.ownerRobotOption}>
<RobotIdentity openInAppUserDetail={false} user={option.user} fallbackId={option.userId} />
<span className={styles.ownerRobotLocation}>
{loadingLocation ? "国家/区域加载中" : robotLocationLabel(option, countryLabels, regionLabels)}
</span>
</div>
);
}
function RequiredLabel({ children }) {
return (
<span>
{children} <span aria-hidden="true">*</span>
</span>
);
}
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")}`;
}
function formatDurationRange(minMs, maxMs) {
return `${formatSeconds(minMs)}-${formatSeconds(maxMs)}`;
}