fix: 空评论列表 500,并完善撤回审计与角色说明

空切片 JSON 统一为数组;撤回保留正文供站长阅读,成员侧居中提示与重新编辑;头像支持拖放;角色弹窗补全职责说明并汇入封禁。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 00:23:55 +08:00
parent 42c395a864
commit 8c5de13899
29 changed files with 835 additions and 405 deletions

View File

@@ -12,17 +12,21 @@ import {
type NotificationItem,
} from "@/lib/api";
import { realtime, RT_NOTIFICATION_NEW } from "@/lib/realtime";
import { onDocumentVisible } from "@/lib/visibilityReconcile";
export default function NotificationBell({ initialUnread = 0 }: { initialUnread?: number }) {
const router = useRouter();
const [open, setOpen] = useState(false);
// SSR 直出未读数,红点不再挂载后才弹出;挂载后轮询自然校正
// SSR 直出未读数,红点不再挂载后才弹出;WS/回前台校准
const [unread, setUnread] = useState(initialUnread);
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
const [loading, setLoading] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const openRef = useRef(open);
openRef.current = open;
const loadNotificationsRef = useRef<() => void>(() => {});
// 未读数:WS 推送后拉取校准;回前台再对账(无定时轮询)
// 未读数:首屏用 SSR;WS 推送 / 回前台校准(无定时轮询;open 变化不重订)
useEffect(() => {
let cancelled = false;
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
@@ -40,22 +44,18 @@ export default function NotificationBell({ initialUnread = 0 }: { initialUnread?
debounceTimer = setTimeout(fetchCount, 200);
};
fetchCount();
const off = realtime.on(RT_NOTIFICATION_NEW, () => {
fetchCountDebounced();
if (open) loadNotifications();
if (openRef.current) loadNotificationsRef.current();
});
const onVisible = () => {
if (document.visibilityState === "visible") fetchCount();
};
document.addEventListener("visibilitychange", onVisible);
const offVisible = onDocumentVisible(fetchCount);
return () => {
cancelled = true;
if (debounceTimer) clearTimeout(debounceTimer);
off();
document.removeEventListener("visibilitychange", onVisible);
offVisible();
};
}, [open]);
}, []);
// 点击外部关闭下拉
useEffect(() => {
@@ -78,6 +78,9 @@ export default function NotificationBell({ initialUnread = 0 }: { initialUnread?
}
setLoading(false);
};
loadNotificationsRef.current = () => {
void loadNotifications();
};
const handleToggle = () => {
const next = !open;