feat: 评论/帖子编辑历史与编辑标记,内容锁定
- 新增 smartDiff 差异计算与 EditHistoryModal 编辑历史弹窗,替换原 CommentEditHistoryModal - 新增 PostEditedMark 编辑标记展示 - 新增内容锁定机制(content_lock)防止并发编辑冲突 - 审核与通知服务适配
This commit is contained in:
@@ -381,11 +381,19 @@ export interface Post {
|
||||
type_status?: string;
|
||||
pinned: number;
|
||||
recommended: boolean;
|
||||
/** 管理员手动锁定:普通用户不可编辑/回复(staff 豁免) */
|
||||
locked: boolean;
|
||||
/** 最后一条已发布评论时间;null=无回复(旧帖判定回落 created_at) */
|
||||
last_reply_at?: string | null;
|
||||
/** 旧帖回复确认提示(详情接口按查看者计算;存在时提交回复前需用户确认) */
|
||||
necro_reply?: { after_hours: number; penalty: number } | null;
|
||||
status: string;
|
||||
like_count: number;
|
||||
view_count: number;
|
||||
comment_count: number;
|
||||
liked: boolean;
|
||||
/** 详情接口返回:是否存在编辑历史快照(列表接口无此字段) */
|
||||
edited?: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
board: Board;
|
||||
@@ -608,7 +616,7 @@ export interface NotificationItem {
|
||||
id: number;
|
||||
user_id: number;
|
||||
actor_id: number;
|
||||
type: "comment" | "reply" | "like" | "approved" | "rejected" | "mention" | "pending_review" | "deleted" | "badge";
|
||||
type: "comment" | "reply" | "like" | "approved" | "rejected" | "mention" | "pending_review" | "deleted" | "badge" | "necro_reply";
|
||||
post_id: number;
|
||||
comment_id: number;
|
||||
room_id?: number;
|
||||
@@ -918,6 +926,14 @@ export interface PublicSettings {
|
||||
points_reply_daily_cap: number;
|
||||
/** 帖子被推荐奖励(每帖仅一次);0=关闭;缺省 0 */
|
||||
points_recommend_reward: number;
|
||||
/** 帖子可编辑时长(小时);0=不锁定;缺省 0;上限 8760 */
|
||||
post_edit_lock_hours: number;
|
||||
/** 评论可编辑时长(小时);0=不锁定;缺省 0;上限 8760 */
|
||||
comment_edit_lock_hours: number;
|
||||
/** 旧帖回复判定时长(小时);0=关闭;缺省 0 */
|
||||
necro_reply_after_hours: number;
|
||||
/** 旧帖回复扣除积分;0=仅提醒不扣分;缺省 0;上限 100 */
|
||||
necro_reply_penalty: number;
|
||||
/** 用户等级体系(数量/每级阈值/每级色板 key);等级数值由后端计算 */
|
||||
levels: LevelDef[];
|
||||
/** 等级特效动画总开关(流光动画);缺省开 */
|
||||
@@ -980,6 +996,10 @@ const DEFAULT_PUBLIC_SETTINGS: PublicSettings = {
|
||||
points_reply_reward: 0,
|
||||
points_reply_daily_cap: 0,
|
||||
points_recommend_reward: 0,
|
||||
post_edit_lock_hours: 0,
|
||||
comment_edit_lock_hours: 0,
|
||||
necro_reply_after_hours: 0,
|
||||
necro_reply_penalty: 0,
|
||||
levels: [...DEFAULT_LEVEL_DEFS],
|
||||
levels_fx: true,
|
||||
};
|
||||
@@ -1068,11 +1088,40 @@ function normalizePublicSettings(data: Partial<PublicSettings> | null | undefine
|
||||
bg_admin_url: typeof data?.bg_admin_url === "string" ? data.bg_admin_url : "",
|
||||
bg_admin_mode: typeof data?.bg_admin_mode === "string" ? data.bg_admin_mode : "cover",
|
||||
...normalizePointsSettings(data),
|
||||
...normalizeLockSettings(data),
|
||||
levels: normalizeLevels(data?.levels),
|
||||
levels_fx: data?.levels_fx !== false,
|
||||
};
|
||||
}
|
||||
|
||||
/** 内容锁定 4 键:小时 0–8760 / 积分 0–100 有效整数,越界/异常回落默认值 */
|
||||
function normalizeLockSettings(
|
||||
data: Partial<PublicSettings> | null | undefined,
|
||||
): Pick<
|
||||
PublicSettings,
|
||||
| "post_edit_lock_hours"
|
||||
| "comment_edit_lock_hours"
|
||||
| "necro_reply_after_hours"
|
||||
| "necro_reply_penalty"
|
||||
> {
|
||||
const hours = (key: keyof PublicSettings): number | undefined => {
|
||||
const v = data?.[key];
|
||||
return typeof v === "number" && v >= 0 && v <= 8760 ? Math.round(v) : undefined;
|
||||
};
|
||||
const points = (key: keyof PublicSettings): number | undefined => {
|
||||
const v = data?.[key];
|
||||
return typeof v === "number" && v >= 0 && v <= 100 ? Math.round(v) : undefined;
|
||||
};
|
||||
return {
|
||||
post_edit_lock_hours: hours("post_edit_lock_hours") ?? DEFAULT_PUBLIC_SETTINGS.post_edit_lock_hours,
|
||||
comment_edit_lock_hours:
|
||||
hours("comment_edit_lock_hours") ?? DEFAULT_PUBLIC_SETTINGS.comment_edit_lock_hours,
|
||||
necro_reply_after_hours:
|
||||
hours("necro_reply_after_hours") ?? DEFAULT_PUBLIC_SETTINGS.necro_reply_after_hours,
|
||||
necro_reply_penalty: points("necro_reply_penalty") ?? DEFAULT_PUBLIC_SETTINGS.necro_reply_penalty,
|
||||
};
|
||||
}
|
||||
|
||||
/** 积分规则 8 键:0–100 有效整数,越界/异常回落默认值 */
|
||||
function normalizePointsSettings(
|
||||
data: Partial<PublicSettings> | null | undefined,
|
||||
@@ -1977,6 +2026,15 @@ export async function apiToggleRecommend(postId: string): Promise<{ recommended:
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** 切换帖子手动锁定(管理员及以上);锁定后普通用户不可编辑/回复 */
|
||||
export async function apiTogglePostLock(postId: string): Promise<{ locked: boolean }> {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/lock`, {
|
||||
method: "PUT",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiChangePassword(oldPassword: string, newPassword: string) {
|
||||
const res = await fetchWithRefresh("/api/change-password", {
|
||||
method: "POST",
|
||||
@@ -3092,6 +3150,10 @@ export type UpdateSiteSettingsBody = {
|
||||
points_reply_reward?: number;
|
||||
points_reply_daily_cap?: number;
|
||||
points_recommend_reward?: number;
|
||||
post_edit_lock_hours?: number;
|
||||
comment_edit_lock_hours?: number;
|
||||
necro_reply_after_hours?: number;
|
||||
necro_reply_penalty?: number;
|
||||
/** 用户等级体系(数量/每级阈值/每级色板 key);整体替换保存 */
|
||||
levels?: LevelDef[];
|
||||
levels_fx?: boolean;
|
||||
@@ -3264,6 +3326,8 @@ export interface AdminContentComment {
|
||||
board_id: number;
|
||||
content: string;
|
||||
status: string;
|
||||
/** 相对创建已编辑(服务端判定);无编辑时隐藏历史入口 */
|
||||
edited: boolean;
|
||||
deleted: boolean;
|
||||
deleted_at?: string;
|
||||
created_at: string;
|
||||
@@ -3352,15 +3416,44 @@ export interface CommentEditHistoryItem {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** 管理:评论编辑历史(分页;默认每页 10 条) */
|
||||
export async function apiAdminCommentHistory(
|
||||
/** 评论编辑历史(登录可见;分页;默认每页 10 条) */
|
||||
export async function apiCommentHistory(
|
||||
commentId: number,
|
||||
page = 1,
|
||||
size = 10
|
||||
): Promise<{ items: CommentEditHistoryItem[]; total: number; page: number; size: number }> {
|
||||
const params = new URLSearchParams({ page: String(page), size: String(size) });
|
||||
const res = await fetchWithRefresh(
|
||||
`/api/admin/content/comments/${commentId}/history?${params}`,
|
||||
`/api/comments/${commentId}/history?${params}`,
|
||||
{ headers: clientHeaders() }
|
||||
);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "获取编辑历史失败");
|
||||
return data;
|
||||
}
|
||||
|
||||
export interface PostEditHistoryItem {
|
||||
id: number;
|
||||
editor: {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
};
|
||||
old_title: string;
|
||||
old_content: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** 帖子编辑历史(登录可见;分页;默认每页 10 条) */
|
||||
export async function apiPostHistory(
|
||||
postId: number,
|
||||
page = 1,
|
||||
size = 10
|
||||
): Promise<{ items: PostEditHistoryItem[]; total: number; page: number; size: number }> {
|
||||
const params = new URLSearchParams({ page: String(page), size: String(size) });
|
||||
const res = await fetchWithRefresh(
|
||||
`/api/posts/${postId}/history?${params}`,
|
||||
{ headers: clientHeaders() }
|
||||
);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
|
||||
Reference in New Issue
Block a user