2026-05-09 13:36:49 +08:00

329 lines
12 KiB
JavaScript

import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
import EditOutlined from "@mui/icons-material/EditOutlined";
import MeetingRoomOutlined from "@mui/icons-material/MeetingRoomOutlined";
import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { IconButton } from "@/shared/ui/IconButton.jsx";
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx";
import { RegionSelect } from "@/shared/ui/RegionSelect.jsx";
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
import { UploadField } from "@/shared/ui/UploadField.jsx";
import { formatMillis } from "@/shared/utils/time.js";
import { RoomDetailDrawer } from "@/features/rooms/components/RoomDetailDrawer.jsx";
import { roomStatusFilters, roomStatusLabels } from "@/features/rooms/constants.js";
import { useRoomsPage } from "@/features/rooms/hooks/useRoomsPage.js";
import styles from "@/features/rooms/rooms.module.css";
const columns = [
{
key: "identity",
label: "房间",
width: "minmax(240px, 1.5fr)",
render: (room) => <RoomIdentity room={room} />,
},
{
key: "owner",
label: "房主",
width: "minmax(210px, 1.2fr)",
render: (room) => <OwnerIdentity room={room} />,
},
{
key: "region",
label: "区域",
width: "minmax(140px, 0.8fr)",
render: (room) => room.regionName || (room.visibleRegionId ? `区域 ${room.visibleRegionId}` : "-"),
},
{
key: "onlineCount",
label: "房间人数",
width: "minmax(110px, 0.7fr)",
render: (room) => `${formatNumber(room.onlineCount)}`,
},
{
key: "status",
label: "状态",
width: "minmax(100px, 0.7fr)",
render: (room) => <RoomStatus status={room.status} />,
},
{
key: "time",
label: "创建时间",
width: "minmax(170px, 1fr)",
render: (room) => formatMillis(room.createdAtMs),
},
];
export function RoomListPage() {
const page = useRoomsPage();
const items = page.data.items || [];
const total = page.data.total || 0;
const tableColumns = [
...columns.map((column) =>
column.key === "region"
? {
...column,
filter: createRegionColumnFilter({
loading: page.loadingRegions,
options: page.regionOptions,
value: page.regionId,
onChange: page.changeRegionId,
}),
}
: column.key === "identity"
? {
...column,
filter: createTextColumnFilter({
placeholder: "搜索房间、房主",
value: page.query,
onChange: page.changeQuery,
}),
render: (room) => <RoomIdentity room={room} onClick={() => page.openDetail(room)} />,
}
: column.key === "status"
? {
...column,
filter: createOptionsColumnFilter({
options: roomStatusFilters,
placeholder: "搜索状态",
value: page.status,
onChange: page.changeStatus,
}),
render: (room) => (
<RoomStatus
disabled={!page.abilities.canUpdate}
status={room.status}
onClick={() => page.openStatus(room)}
/>
),
}
: column,
),
{
key: "actions",
label: "操作",
width: "minmax(104px, 0.6fr)",
render: (room) => <RoomActions page={page} room={room} />,
},
];
return (
<section className={styles.root}>
<div className={styles.contentPanel}>
<div className={styles.toolbar}>
<section className={styles.filters}>
<AdminFilterResetButton
disabled={!page.query && !page.status && !page.regionId}
onClick={page.resetFilters}
/>
</section>
</div>
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
<div className={styles.listBlock}>
<DataTable
columns={tableColumns}
items={items}
minWidth="1180px"
rowKey={(room) => room.roomId}
/>
{total > 0 ? (
<PaginationBar
page={page.page}
pageSize={page.data.pageSize || 10}
total={total}
onPageChange={page.setPage}
/>
) : null}
</div>
</DataState>
</div>
<RoomDetailDrawer open={page.activeAction === "detail"} room={page.activeRoom} onClose={page.closeAction} />
<ActionModal
disabled={!page.abilities.canUpdate}
loading={page.loadingAction === "edit"}
open={page.activeAction === "edit"}
title="编辑房间"
onClose={page.closeAction}
onSubmit={page.submitEdit}
>
<TextField
disabled={!page.abilities.canUpdate}
label="房间名称"
required
value={page.form.title}
onChange={(event) => page.setForm({ ...page.form, title: event.target.value })}
/>
<UploadField
disabled={!page.abilities.canUpdate || !page.abilities.canUpload}
label="房间头像"
value={page.form.coverUrl}
onChange={(coverUrl) => page.setForm({ ...page.form, coverUrl })}
/>
<RegionSelect
disabled={!page.abilities.canUpdate}
emptyLabel="选择区域"
loading={page.loadingRegions}
options={page.regionOptions}
required
value={page.form.visibleRegionId}
onChange={(visibleRegionId) => page.setForm({ ...page.form, visibleRegionId })}
/>
<TextField
disabled={!page.abilities.canUpdate}
label="房间简介"
multiline
minRows={2}
value={page.form.description}
onChange={(event) => page.setForm({ ...page.form, description: event.target.value })}
/>
</ActionModal>
<ActionModal
disabled={!page.abilities.canUpdate}
loading={page.loadingAction === "status"}
open={page.activeAction === "status"}
title="房间状态"
onClose={page.closeAction}
onSubmit={page.submitStatus}
>
<TextField
disabled={!page.abilities.canUpdate}
label="状态"
required
select
value={page.statusForm.status}
onChange={(event) => page.setStatusForm({ status: event.target.value })}
>
<MenuItem value="active">正常</MenuItem>
<MenuItem value="closed">关闭</MenuItem>
</TextField>
</ActionModal>
</section>
);
}
function RoomIdentity({ onClick, room }) {
const content = (
<div className={styles.identity}>
<span className={styles.cover}>
{room.coverUrl ? <img src={room.coverUrl} alt="" /> : <MeetingRoomOutlined fontSize="small" />}
</span>
<div className={styles.identityText}>
<div className={styles.name}>{room.title || "-"}</div>
<div className={styles.meta}>{room.roomId}</div>
</div>
</div>
);
if (!onClick) {
return content;
}
return (
<button
aria-label={`查看房间详情:${room.title || room.roomId}`}
className={styles.identityButton}
type="button"
onClick={onClick}
>
{content}
</button>
);
}
function OwnerIdentity({ room }) {
const owner = room.owner || {};
const ownerName = owner.username || "-";
const ownerDisplayId = owner.displayUserId || room.ownerUserId || "-";
return (
<div className={styles.identity}>
<span className={styles.ownerAvatar}>
{owner.avatar ? <img src={owner.avatar} alt="" /> : <PersonOutlineOutlined fontSize="small" />}
</span>
<div className={styles.identityText}>
<div className={styles.name}>{ownerName}</div>
<div className={styles.meta}>{ownerDisplayId}</div>
</div>
</div>
);
}
function RoomActions({ page, room }) {
return (
<div className={styles.rowActions}>
{page.abilities.canUpdate ? (
<Tooltip arrow title="编辑">
<span>
<IconButton label="编辑" onClick={() => page.openEdit(room)}>
<EditOutlined fontSize="small" />
</IconButton>
</span>
</Tooltip>
) : null}
{page.abilities.canDelete ? (
<Tooltip arrow title="删除">
<span>
<IconButton
disabled={page.loadingAction === `delete-${room.roomId}`}
label="删除"
tone="danger"
onClick={() => page.removeRoom(room)}
>
<DeleteOutlineOutlined fontSize="small" />
</IconButton>
</span>
</Tooltip>
) : null}
</div>
);
}
function RoomStatus({ disabled, onClick, status }) {
const normalizedStatus = status === "active" ? "active" : "closed";
const tone = normalizedStatus === "active" ? "running" : "stopped";
const badge = (
<span className={`status-badge status-badge--${tone}`}>
<span className="status-point" />
{roomStatusLabels[normalizedStatus]}
</span>
);
if (disabled) {
return badge;
}
return (
<button className={styles.statusButton} type="button" onClick={onClick}>
{badge}
</button>
);
}
function ActionModal({ children, disabled, loading, onClose, onSubmit, open, title }) {
return (
<AdminFormDialog
loading={loading}
open={open}
size="compact"
submitDisabled={disabled || loading}
title={title}
onClose={onClose}
onSubmit={onSubmit}
>
{children}
</AdminFormDialog>
);
}
function formatNumber(value) {
return Number(value || 0).toLocaleString("zh-CN");
}