356 lines
13 KiB
JavaScript
356 lines
13 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 TextField from "@mui/material/TextField";
|
|
import Tooltip from "@mui/material/Tooltip";
|
|
import { AdminFormDialog, AdminFormSection } from "@/shared/ui/AdminFormDialog.jsx";
|
|
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
|
import { IconButton } from "@/shared/ui/IconButton.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 || "-",
|
|
},
|
|
{
|
|
key: "onlineCount",
|
|
label: "房间人数",
|
|
width: "minmax(110px, 0.7fr)",
|
|
render: (room) => `${formatNumber(room.onlineCount)} 人`,
|
|
},
|
|
{
|
|
key: "roomContribution",
|
|
label: "房间贡献",
|
|
width: "minmax(120px, 0.8fr)",
|
|
render: (room) => formatNumber(roomContributionValue(room)),
|
|
},
|
|
{
|
|
key: "status",
|
|
label: "状态",
|
|
width: "minmax(100px, 0.7fr)",
|
|
render: (room) => roomStatusLabels[room.status === "active" ? "active" : "closed"],
|
|
},
|
|
{
|
|
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) => <RoomStatusSwitch page={page} room={room} />,
|
|
}
|
|
: column.key === "roomContribution"
|
|
? {
|
|
...column,
|
|
header: (
|
|
<ContributionSortHeader
|
|
direction={page.contributionSortDirection}
|
|
onClick={page.toggleContributionSort}
|
|
/>
|
|
),
|
|
}
|
|
: 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}>
|
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
|
<div className={styles.listBlock}>
|
|
<DataTable
|
|
columns={tableColumns}
|
|
items={items}
|
|
minWidth="1180px"
|
|
pagination={
|
|
total > 0
|
|
? {
|
|
page: page.page,
|
|
pageSize: page.data.pageSize || 50,
|
|
total,
|
|
onPageChange: page.setPage,
|
|
}
|
|
: undefined
|
|
}
|
|
rowKey={(room) => room.roomId}
|
|
/>
|
|
</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"}
|
|
sectionTitle="房间信息"
|
|
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-${page.activeRoom?.roomId}`}
|
|
open={page.activeAction === "status"}
|
|
sectionTitle="关闭信息"
|
|
title="关闭房间"
|
|
onClose={page.closeAction}
|
|
onSubmit={page.submitStatus}
|
|
>
|
|
<TextField
|
|
disabled={!page.abilities.canUpdate}
|
|
label="关闭原因"
|
|
multiline
|
|
minRows={3}
|
|
required
|
|
value={page.statusForm.closeReason}
|
|
onChange={(event) => page.setStatusForm({ ...page.statusForm, closeReason: event.target.value })}
|
|
/>
|
|
</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>
|
|
</div>
|
|
);
|
|
|
|
if (!onClick) {
|
|
return content;
|
|
}
|
|
|
|
return (
|
|
<button
|
|
aria-label={`查看房间详情:${room.title || "未命名房间"}`}
|
|
className={styles.identityButton}
|
|
type="button"
|
|
onClick={onClick}
|
|
>
|
|
{content}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function OwnerIdentity({ room }) {
|
|
const owner = room.owner || {};
|
|
const ownerName = owner.username || "-";
|
|
const ownerDisplayId = ownerLongShortId(owner, room);
|
|
|
|
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 RoomStatusSwitch({ page, room }) {
|
|
const checked = room.status === "active";
|
|
return (
|
|
<div className={styles.statusCell}>
|
|
<AdminSwitch
|
|
checked={checked}
|
|
checkedLabel="正常"
|
|
disabled={!page.abilities.canUpdate || page.loadingAction === `status-${room.roomId}`}
|
|
inputProps={{ "aria-label": checked ? "关闭房间" : "恢复房间" }}
|
|
uncheckedLabel="关闭"
|
|
onChange={(event) => page.toggleRoomStatus(room, event.target.checked)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ActionModal({ children, disabled, loading, onClose, onSubmit, open, sectionTitle = "基本信息", title }) {
|
|
return (
|
|
<AdminFormDialog
|
|
loading={loading}
|
|
open={open}
|
|
size="compact"
|
|
submitDisabled={disabled || loading}
|
|
title={title}
|
|
onClose={onClose}
|
|
onSubmit={onSubmit}
|
|
>
|
|
<AdminFormSection title={sectionTitle}>{children}</AdminFormSection>
|
|
</AdminFormDialog>
|
|
);
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
return Number(value || 0).toLocaleString("zh-CN");
|
|
}
|
|
|
|
function roomContributionValue(room) {
|
|
return room.roomContribution ?? room.heat ?? 0;
|
|
}
|
|
|
|
function ContributionSortHeader({ direction, onClick }) {
|
|
const active = direction === "asc" || direction === "desc";
|
|
const arrow = direction === "asc" ? "↑" : "↓";
|
|
return (
|
|
<button
|
|
aria-label={`按房间贡献${direction === "desc" ? "正序" : "倒序"}排序`}
|
|
className={[styles.sortHeader, active ? styles.sortHeaderActive : ""].filter(Boolean).join(" ")}
|
|
type="button"
|
|
onClick={onClick}
|
|
>
|
|
<span>房间贡献</span>
|
|
<span className={styles.sortArrow}>{active ? arrow : "↕"}</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function ownerLongShortId(owner, room) {
|
|
const longId = owner.userId || room.ownerUserId || "";
|
|
const shortId = owner.displayUserId || "";
|
|
if (longId && shortId && longId !== shortId) {
|
|
return `${longId}(${shortId})`;
|
|
}
|
|
return longId || shortId || "-";
|
|
}
|