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

@@ -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;
}
// 单群未读明细

View File

@@ -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 / 会话列表角标)

View File

@@ -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:
"无任何后台入口。可发帖、评论、私信等;新帖与评论按站点规则进入审核。降级为普通用户时会清空板块授权。",
},
};

View 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);
}