335 lines
12 KiB
JavaScript
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 "区域置顶";
|
|
}
|