fix: 空评论列表 500,并完善撤回审计与角色说明
空切片 JSON 统一为数组;撤回保留正文供站长阅读,成员侧居中提示与重新编辑;头像支持拖放;角色弹窗补全职责说明并汇入封禁。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -460,7 +460,9 @@ export async function fetchUserComments(id: string, page = 1): Promise<UserComme
|
||||
const res = await fetch(`${API_BASE}/api/users/${id}/comments?${params}`, ssrInit());
|
||||
if (res.status === 404) throw new Error("用户不存在");
|
||||
if (!res.ok) throw new Error("获取评论失败");
|
||||
return res.json();
|
||||
const data = (await res.json()) as UserCommentsResponse;
|
||||
// Go 空切片曾序列化为 null;统一归一成 [],避免 SSR 读 .length 崩
|
||||
return { ...data, comments: data.comments ?? [] };
|
||||
}
|
||||
|
||||
// SSR 直出通知列表(仅登录用户;401 由页面层转为登录引导)
|
||||
@@ -1225,6 +1227,8 @@ export interface ChatMessage {
|
||||
recalled_at?: string | null;
|
||||
recalled_by?: number;
|
||||
sender: User;
|
||||
/** 撤回操作者(已撤回时后端 Preload;本人撤回时通常等于 sender) */
|
||||
recaller?: User | null;
|
||||
}
|
||||
|
||||
// 单群未读明细
|
||||
|
||||
@@ -72,6 +72,13 @@ export interface RtChatRecalledData {
|
||||
sender_id?: number;
|
||||
recalled_by: number;
|
||||
recalled_at?: string;
|
||||
recaller?: {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
role?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 未读增量(推送给未在房内订阅的成员,用于 Header / 会话列表角标)
|
||||
|
||||
@@ -86,6 +86,11 @@ export interface RoleMeta {
|
||||
/** 主色使用的 CSS token 名(RoleBadge 取 var(--token)) */
|
||||
colorToken: string;
|
||||
softToken: string;
|
||||
/**
|
||||
* 授权弹窗用的职责说明(站长不出现在可授权列表)。
|
||||
* 只描述该角色「有什么」,最终权限以后端为准。
|
||||
*/
|
||||
grantDesc?: string;
|
||||
}
|
||||
|
||||
// 各管理角色的图标与配色集中配置;站长固定使用皇冠站长标识
|
||||
@@ -96,6 +101,8 @@ export const ROLE_META: Record<RoleKey, RoleMeta> = {
|
||||
icon: "crown",
|
||||
colorToken: "--gold",
|
||||
softToken: "--gold-soft",
|
||||
grantDesc:
|
||||
"站点所有者:全部后台能力、群聊监管、用户内容档案;身份不可授予或降级。",
|
||||
},
|
||||
[ROLES.SUPER_ADMIN]: {
|
||||
key: ROLES.SUPER_ADMIN,
|
||||
@@ -103,6 +110,8 @@ export const ROLE_META: Record<RoleKey, RoleMeta> = {
|
||||
icon: "shield-star",
|
||||
colorToken: "--accent",
|
||||
softToken: "--accent-soft",
|
||||
grantDesc:
|
||||
"用户与角色管理、封禁、外观主题、公告;全站内容审核与置顶/加精;群聊监管(可见全部会话、可撤回任意消息)。不可动站长账号,亦无站长专属的用户内容档案。",
|
||||
},
|
||||
[ROLES.ADMIN]: {
|
||||
key: ROLES.ADMIN,
|
||||
@@ -110,6 +119,8 @@ export const ROLE_META: Record<RoleKey, RoleMeta> = {
|
||||
icon: "shield",
|
||||
colorToken: "--accent",
|
||||
softToken: "--accent-soft",
|
||||
grantDesc:
|
||||
"公告管理;全站帖子/评论审核;前台置顶与加精。不含用户权限、外观设置与群聊监管旁路。",
|
||||
},
|
||||
[ROLES.BOARD_ADMIN]: {
|
||||
key: ROLES.BOARD_ADMIN,
|
||||
@@ -117,6 +128,8 @@ export const ROLE_META: Record<RoleKey, RoleMeta> = {
|
||||
icon: "shield-board",
|
||||
colorToken: "--accent",
|
||||
softToken: "--accent-soft",
|
||||
grantDesc:
|
||||
"仅审核并管理被授权板块内的帖子与评论(含删除违规内容)。无公告、置顶/加精、用户管理与外观设置;须至少勾选一个板块。",
|
||||
},
|
||||
[ROLES.USER]: {
|
||||
key: ROLES.USER,
|
||||
@@ -124,6 +137,8 @@ export const ROLE_META: Record<RoleKey, RoleMeta> = {
|
||||
icon: "user",
|
||||
colorToken: "--ink-3",
|
||||
softToken: "--panel-2",
|
||||
grantDesc:
|
||||
"无任何后台入口。可发帖、评论、私信等;新帖与评论按站点规则进入审核。降级为普通用户时会清空板块授权。",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
31
frontend/lib/visibilityReconcile.ts
Normal file
31
frontend/lib/visibilityReconcile.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 回前台 HTTP 对账限频。
|
||||
*
|
||||
* 多组件(Header / 通知铃 / 聊天侧栏 / ThemeSync / 管理后台)都会在
|
||||
* visibilitychange 时拉一次状态;短时间切标签会叠加请求。
|
||||
* 本工具保证「同一订阅在 minGapMs 内最多执行一次」。
|
||||
*/
|
||||
|
||||
/** 与 ThemeSync 对齐:10s 内不重复对账 */
|
||||
export const VISIBLE_RECONCILE_GAP_MS = 10_000;
|
||||
|
||||
/**
|
||||
* 监听 document 重新可见;限频后调用 run。
|
||||
* 返回取消订阅函数。
|
||||
*/
|
||||
export function onDocumentVisible(
|
||||
run: () => void,
|
||||
minGapMs: number = VISIBLE_RECONCILE_GAP_MS
|
||||
): () => void {
|
||||
if (typeof document === "undefined") return () => {};
|
||||
let lastAt = 0;
|
||||
const onVisible = () => {
|
||||
if (document.visibilityState !== "visible") return;
|
||||
const now = Date.now();
|
||||
if (now - lastAt < minGapMs) return;
|
||||
lastAt = now;
|
||||
run();
|
||||
};
|
||||
document.addEventListener("visibilitychange", onVisible);
|
||||
return () => document.removeEventListener("visibilitychange", onVisible);
|
||||
}
|
||||
Reference in New Issue
Block a user