2026-06-23 20:47:41 +08:00

335 lines
12 KiB
JavaScript

import AddOutlined from "@mui/icons-material/AddOutlined";
import CloseOutlined from "@mui/icons-material/CloseOutlined";
import MeetingRoomOutlined from "@mui/icons-material/MeetingRoomOutlined";
import Autocomplete from "@mui/material/Autocomplete";
import MenuItem from "@mui/material/MenuItem";
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 { IconButton } from "@/shared/ui/IconButton.jsx";
import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx";
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
import { formatMillis } from "@/shared/utils/time.js";
import { useRoomPinsPage } from "@/features/rooms/hooks/useRoomPinsPage.js";
import styles from "@/features/rooms/rooms.module.css";
const pinStatusOptions = [
{ label: "有效", value: "active" },
{ label: "已过期", value: "expired" },
{ label: "已取消", value: "cancelled" },
{ label: "全部", value: "all" },
];
const pinTypeOptions = [
{ label: "区域置顶", value: "region" },
{ label: "国家置顶", value: "country" },
{ label: "全区置顶", value: "global" },
];
const columns = [
{
key: "room",
label: "房间信息",
width: "minmax(280px, 1.5fr)",
render: (pin) => <PinRoomIdentity room={pin.room} />,
},
{
key: "user",
label: "用户信息",
width: "minmax(240px, 1.2fr)",
render: (pin) => <PinUserIdentity user={pin.user} />,
},
{
key: "region",
label: "区域",
width: "minmax(140px, 0.7fr)",
render: (pin) => pin.room?.regionName || (pin.room?.visibleRegionId ? `区域 ${pin.room.visibleRegionId}` : "-"),
},
{
key: "weight",
label: "排序",
width: "minmax(100px, 0.5fr)",
render: (pin) => Number(pin.weight || 0).toLocaleString("zh-CN"),
},
{
key: "pinType",
label: "置顶类型",
width: "minmax(130px, 0.7fr)",
render: (pin) => pinTypeLabel(pin.pinType),
},
{
key: "pinnedAt",
label: "置顶时间",
width: "minmax(170px, 0.9fr)",
render: (pin) => formatMillis(pin.pinnedAtMs),
},
{
key: "remaining",
label: "剩余时间",
width: "minmax(150px, 0.8fr)",
render: (pin) => formatRemaining(pin),
},
{
key: "expiresAt",
label: "到期时间",
width: "minmax(170px, 0.9fr)",
render: (pin) => formatMillis(pin.expiresAtMs),
},
];
export function RoomPinsPage() {
const page = useRoomPinsPage();
const items = page.data.items || [];
const total = page.data.total || 0;
const tableColumns = [
...columns.map((column) =>
column.key === "room"
? {
...column,
filter: createTextColumnFilter({
placeholder: "搜索房间 ID",
value: page.query,
onChange: page.changeQuery,
}),
}
: column.key === "region"
? {
...column,
filter: createRegionColumnFilter({
loading: page.loadingRegions,
options: page.regionOptions,
value: page.regionId,
onChange: page.changeRegionId,
}),
}
: column.key === "remaining"
? {
...column,
filter: createOptionsColumnFilter({
options: pinStatusOptions,
placeholder: "置顶状态",
value: page.status,
onChange: page.changeStatus,
}),
}
: column,
),
{
key: "actions",
label: "操作",
width: "minmax(104px, 0.6fr)",
render: (pin) => <RoomPinActions page={page} pin={pin} />,
},
];
return (
<section className={styles.root}>
<div className={styles.contentPanel}>
<div className={styles.toolbar}>
<section className={styles.filters}>
<AdminFilterResetButton
disabled={!page.query && page.status === "active" && !page.regionId}
onClick={page.resetFilters}
/>
</section>
{page.abilities.canPinCreate ? (
<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="1480px"
rowKey={(pin) => pin.id}
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="compact"
submitDisabled={!page.abilities.canPinCreate || page.loadingAction === "create"}
title="新增房间置顶"
onClose={page.closeAction}
onSubmit={page.submitCreate}
>
<Autocomplete
filterOptions={(options) => options}
getOptionLabel={(option) => roomOptionLabel(option)}
isOptionEqualToValue={(option, value) => option.roomId === value.roomId}
loading={page.roomOptionsLoading}
noOptionsText="当前无数据"
inputValue={page.roomInput}
options={page.roomOptions}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(page.roomOptionsError)}
helperText={page.roomOptionsError}
label="选择房间"
placeholder="输入用户短ID / 房间ID"
required
/>
)}
renderOption={(props, option) => (
<li {...props} key={option.roomId}>
<PinRoomIdentity room={option} />
</li>
)}
size="small"
value={page.selectedRoom}
onChange={(_, option) => page.selectRoom(option)}
onInputChange={(_, value, reason) => {
if (reason === "input") {
page.changeRoomInput(value);
}
if (reason === "clear") {
page.selectRoom(null);
}
}}
/>
<TextField
label="置顶类型"
required
select
value={page.form.pinType}
onChange={(event) => page.setForm({ ...page.form, pinType: event.target.value })}
>
{pinTypeOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
<TextField
label="排序"
required
type="number"
value={page.form.weight}
onChange={(event) => page.setForm({ ...page.form, weight: event.target.value })}
/>
<TextField
InputLabelProps={{ shrink: true }}
label="开始时间"
required
type="datetime-local"
value={page.form.pinnedAtMs}
onChange={(event) => page.setForm({ ...page.form, pinnedAtMs: event.target.value })}
/>
<TextField
InputLabelProps={{ shrink: true }}
label="结束时间"
required
type="datetime-local"
value={page.form.expiresAtMs}
onChange={(event) => page.setForm({ ...page.form, expiresAtMs: event.target.value })}
/>
</AdminFormDialog>
</section>
);
}
function PinRoomIdentity({ room = {} }) {
return (
<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}>长ID {room.roomId || "-"}</div>
<div className={styles.meta}>短ID {room.roomShortId || "-"}</div>
</div>
</div>
);
}
function PinUserIdentity({ user = {} }) {
return <AdminUserIdentity openInAppUserDetail rows={[user.displayUserId, user.userId]} user={user} />;
}
function RoomPinActions({ page, pin }) {
const canCancel = page.abilities.canPinCancel && pin.status === "active" && Number(pin.remainingMs || 0) > 0;
if (!canCancel) {
return <span className={styles.meta}>-</span>;
}
return (
<div className={styles.rowActions}>
<Tooltip arrow title="取消置顶">
<span>
<IconButton
disabled={page.loadingAction === `cancel-${pin.id}`}
label="取消置顶"
tone="danger"
onClick={() => page.cancelPin(pin)}
>
<CloseOutlined fontSize="small" />
</IconButton>
</span>
</Tooltip>
</div>
);
}
function roomOptionLabel(room) {
if (!room) {
return "";
}
return [room.title, room.roomShortId, room.roomId].filter(Boolean).join(" / ");
}
function formatRemaining(pin) {
if (pin.status === "cancelled") {
return "-";
}
const ms = Number(pin.remainingMs || 0);
if (ms <= 0) {
return "0天";
}
const dayMs = 24 * 60 * 60 * 1000;
const days = Math.floor(ms / dayMs);
const hours = Math.floor((ms % dayMs) / (60 * 60 * 1000));
if (days > 0) {
return `${days}${hours > 0 ? `${hours}小时` : ""}`;
}
return `${Math.max(hours, 1)}小时`;
}
function pinTypeLabel(pinType) {
if (pinType === "global") {
return "全区置顶";
}
if (pinType === "country") {
return "国家置顶";
}
return "区域置顶";
}