187 lines
6.0 KiB
JavaScript
187 lines
6.0 KiB
JavaScript
import ContentCopyOutlined from "@mui/icons-material/ContentCopyOutlined";
|
||
import MeetingRoomOutlined from "@mui/icons-material/MeetingRoomOutlined";
|
||
import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
|
||
import { useEffect, useState } from "react";
|
||
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
|
||
import { formatMillis } from "@/shared/utils/time.js";
|
||
import { roomStatusLabels } from "@/features/rooms/constants.js";
|
||
import styles from "@/features/rooms/rooms.module.css";
|
||
|
||
export function RoomDetailDrawer({ onClose, open, room }) {
|
||
const owner = room?.owner || {};
|
||
const normalizedStatus = room?.status === "active" ? "active" : "closed";
|
||
const ownerDisplayId = ownerLongShortId(owner, room);
|
||
|
||
return (
|
||
<SideDrawer open={open} title="房间详情" width="wide" onClose={onClose}>
|
||
{room ? (
|
||
<>
|
||
<header className={styles.detailHero}>
|
||
<span className={styles.detailCover}>
|
||
{room.coverUrl ? <img src={room.coverUrl} alt="" /> : <MeetingRoomOutlined fontSize="medium" />}
|
||
</span>
|
||
<div className={styles.detailHeroText}>
|
||
<div className={styles.detailTitleRow}>
|
||
<h3>{room.title || "-"}</h3>
|
||
<StatusBadge status={normalizedStatus} />
|
||
</div>
|
||
<CopyableRoomId className={styles.detailMeta} prefix="房间 ID " roomId={room.roomId} />
|
||
</div>
|
||
</header>
|
||
|
||
<DetailSection
|
||
items={[
|
||
["房间名称", room.title],
|
||
["房间 ID", <CopyableRoomId key="room-id" roomId={room.roomId} />],
|
||
["房间状态", roomStatusLabels[normalizedStatus]],
|
||
["房间模式", room.mode],
|
||
["区域", room.regionName],
|
||
["创建时间", formatMillis(room.createdAtMs)],
|
||
["更新时间", formatMillis(room.updatedAtMs)]
|
||
]}
|
||
title="基础信息"
|
||
/>
|
||
|
||
<section className={styles.detailSection}>
|
||
<h3 className={styles.detailSectionTitle}>房主信息</h3>
|
||
<div className={styles.detailOwner}>
|
||
<span className={styles.detailOwnerAvatar}>
|
||
{owner.avatar ? <img src={owner.avatar} alt="" /> : <PersonOutlineOutlined fontSize="small" />}
|
||
</span>
|
||
<div className={styles.detailOwnerText}>
|
||
<strong>{owner.username || "-"}</strong>
|
||
<span>{ownerDisplayId}</span>
|
||
</div>
|
||
</div>
|
||
<div className={styles.detailGrid}>
|
||
<DetailItem label="房主长 ID(短 ID)" value={ownerDisplayId} />
|
||
</div>
|
||
</section>
|
||
|
||
<DetailSection
|
||
items={[
|
||
["在线人数", `${formatNumber(room.onlineCount)} 人`],
|
||
["座位数", `${formatNumber(room.seatCount)} 个`],
|
||
["已占座位", `${formatNumber(room.occupiedSeatCount)} 个`],
|
||
["房间贡献", formatNumber(roomContributionValue(room))],
|
||
["热度", formatNumber(room.heat)],
|
||
["排序分", formatNumber(room.sortScore)]
|
||
]}
|
||
title="房间数据"
|
||
/>
|
||
</>
|
||
) : null}
|
||
</SideDrawer>
|
||
);
|
||
}
|
||
|
||
function DetailSection({ items, title }) {
|
||
return (
|
||
<section className={styles.detailSection}>
|
||
<h3 className={styles.detailSectionTitle}>{title}</h3>
|
||
<div className={styles.detailGrid}>
|
||
{items.map(([label, value]) => (
|
||
<DetailItem key={label} label={label} value={value} />
|
||
))}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function DetailItem({ label, value }) {
|
||
return (
|
||
<div className={styles.detailItem}>
|
||
<span className={styles.detailLabel}>{label}</span>
|
||
<div className={styles.detailValue}>{displayValue(value)}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function CopyableRoomId({ className = "", prefix = "", roomId }) {
|
||
const [copied, setCopied] = useState(false);
|
||
const value = displayValue(roomId);
|
||
|
||
useEffect(() => {
|
||
if (!copied) {
|
||
return undefined;
|
||
}
|
||
const timer = window.setTimeout(() => setCopied(false), 1200);
|
||
return () => window.clearTimeout(timer);
|
||
}, [copied]);
|
||
|
||
const copyRoomId = async () => {
|
||
if (!roomId) {
|
||
return;
|
||
}
|
||
await copyText(String(roomId));
|
||
setCopied(true);
|
||
};
|
||
|
||
return (
|
||
<button
|
||
aria-label={`复制房间 ID ${value}`}
|
||
className={[styles.copyValue, className].filter(Boolean).join(" ")}
|
||
disabled={!roomId}
|
||
type="button"
|
||
onClick={copyRoomId}
|
||
>
|
||
<span>{prefix}{value}</span>
|
||
<ContentCopyOutlined className={styles.copyIcon} fontSize="inherit" />
|
||
{copied ? <span className={styles.copyState}>已复制</span> : null}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
function StatusBadge({ status }) {
|
||
const tone = status === "active" ? "running" : "stopped";
|
||
|
||
return (
|
||
<span className={`status-badge status-badge--${tone}`}>
|
||
<span className="status-point" />
|
||
{roomStatusLabels[status]}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function displayValue(value) {
|
||
return value === 0 || value ? value : "-";
|
||
}
|
||
|
||
function formatNumber(value) {
|
||
return Number(value || 0).toLocaleString("zh-CN");
|
||
}
|
||
|
||
function roomContributionValue(room) {
|
||
return room?.roomContribution ?? room?.heat ?? 0;
|
||
}
|
||
|
||
async function copyText(value) {
|
||
if (navigator.clipboard?.writeText) {
|
||
try {
|
||
await navigator.clipboard.writeText(value);
|
||
return;
|
||
} catch {
|
||
// 本地开发和部分浏览器策略会拒绝 Clipboard API,退回同步复制路径。
|
||
}
|
||
}
|
||
|
||
const input = document.createElement("textarea");
|
||
input.value = value;
|
||
input.setAttribute("readonly", "");
|
||
input.style.position = "fixed";
|
||
input.style.opacity = "0";
|
||
document.body.appendChild(input);
|
||
input.select();
|
||
document.execCommand("copy");
|
||
input.remove();
|
||
}
|
||
|
||
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 || "-";
|
||
}
|