成为全栈·React 管理后台篇·评论审核工作流:把状态下拉改成可理解的动作

成为全栈·React 管理后台篇·评论审核工作流:把状态下拉改成可理解的动作

审核人员需要判断内容并采取动作,而不是替系统翻译状态枚举。界面要同时提供原文、上下文、理由和明确结果。

前言

评论审核页最初很像一张数据库管理表:每行显示内容和 status,点击编辑后从 approved、rejected、reviewing 中选一个目标值。功能齐全,却很难用。审核者必须先记住英文状态含义,再猜"改成 reviewing"会发生什么。

更严重的是,单看一句回复往往无法判断语气。它回复了哪篇文章、针对哪条父评论、为什么上次被拒绝,这些上下文都不在对话框里。审核动作本身很简单,决策所需的信息却被拆散了。

三个状态,要翻译成三个动作

后端评论状态机包含:approved 表示公开可见,rejected 表示拒绝,reviewing 表示进入人工复核。界面将它们展示为"通过""拒绝""待复核"三个单选动作,同时保留英文状态供开发沟通。

单选比下拉更适合这里,因为选项只有三个,而且每个选项会改变下方表单:通过时无需理由;拒绝时显示"拒绝理由";待复核时显示"复核备注"。所有结果同时可见,审核者不用展开控件逐个寻找。

tsx 复制代码
{STATUS_OPTIONS.map((option) => (
  <label key={option.value}>
    <input type="radio" value={option.value} {...form.register('status')} />
    {option.label}
  </label>
))}

{targetStatus !== 'approved' && (
  <TextAreaField
    name="reason"
    label={targetStatus === 'reviewing' ? '复核备注' : '拒绝理由'}
  />
)}

审核内容必须带上下文

对话框首先显示作者、评论 id 和完整评论内容,再通过 CommentContext 查询文章及父评论。审核者可以确认这句话出现在哪篇文章下、是在回应谁,而不必关闭弹窗另开页面。

历史拒绝理由也保留展示。重新审核时,过去为什么拒绝是决策证据,不应因为打开新表单就消失。

上下文请求失败时,核心评论仍可审核;辅助信息应显示局部失败,而不是让整个弹窗不可用。反过来,评论正文加载失败时就不能假装有足够信息做决定。

理由要跟状态一起提交

后端在批准评论时会清空旧的 rejectedReason,因此通过动作不应把表单中残留的理由重新提交:

ts 复制代码
onSubmit(comment.id, {
  status: values.status,
  reason: values.status === 'approved'
    ? undefined
    : values.reason.trim() || undefined,
})

理由上限 200 字,与契约一致,并显示实时字数。每次打开另一条评论时,表单都按该评论当前状态和历史理由 reset,避免上一条输入串到下一条。

提交期间禁止按 Escape、点击遮罩或重复提交。审核请求是否成功尚未确定时关闭弹窗,会让用户失去结果上下文。

提示文案必须说清结果

统一显示"状态已更新"几乎没有信息。mutation 根据目标状态提供不同反馈:

ts 复制代码
const MODERATE_TOAST = {
  approved: '已通过审核',
  rejected: '已拒绝该评论',
  reviewing: '已标记为待人工复核',
}

成功后失效评论列表,让当前筛选下的数据重新确认。若正在查看"待复核",通过一条评论后它从列表消失是正确结果;提示文案能解释这次变化。

官方回复也要经过内容安全流程

editor/admin 的代回复复用普通发表评论接口,因此仍会经过敏感词过滤。返回的评论可能直接是 rejected,前端不能乐观插入列表。

ts 复制代码
onSuccess: (comment) => {
  if (comment.status === 'rejected') {
    toast.info('回复未发布,请修改后重试')
  } else {
    toast.success('回复已发布')
  }
  invalidate()
}

回复被拒绝时,对话框展示原因并保留原输入。用户只需修改问题内容,不必重新写一遍。这体现了失败恢复的基本原则:失败结果来自服务器,但工作副本仍属于用户。

"官方身份"不意味着绕过审核。否则攻击者只要获取后台账号,就能借高权限发布前台本应阻止的内容,也会让同一套内容规则出现两个标准。

reviewing 不是自动流程的普通产物

当前契约中,自动发表只产生 approved 或 rejected,reviewing 是人工复核的兜底态,并由审核接口进出。前端不能因为有三个枚举值,就假设每条新评论都可能随机落入三态。

理解状态由哪条命令产生很重要。状态机不是一组可任意赋值的字符串,而是事件、权限和副作用的集合。若以后加入机器置信度,把边界内容自动送入 reviewing,界面还需要展示触发原因和模型证据,而不只是多一个筛选项。

列表筛选是审核队列,不是普通搜索

评论列表中的状态筛选实际上定义了不同工作队列。reviewing 是需要人工判断的入口,rejected 适合复查规则是否误伤,approved 则用于处理已经公开但被举报或发现问题的内容。

审核成功后,当前评论可能不再属于当前队列。例如在 reviewing 筛选中点击通过,刷新后该行消失。此时应保留原筛选和页码,只更新服务器事实;如果顺手清空筛选回到全部评论,审核者会失去工作位置。

批量审核还要面对部分失败。某条评论可能已经被另一位编辑处理,另一条因权限变化失败。不能统一提示"全部成功",也不能因为一项失败就隐藏其余成功结果。当前批量基础设施会顺序执行、汇总一次提示,并保留失败项供重试;详细机制留到 M2-15。

删除与审核是两种不同决定

拒绝评论保留记录和理由,便于复查内容规则;删除则可能按契约级联删除子回复,影响整段讨论。两者不能合并成"让内容不可见"一个按钮。

删除确认应说明子回复影响,并在成功后失效评论缓存。若只是内容不合规,通常先拒绝并记录理由;只有垃圾数据、违规风险或用户明确删除需求时,才使用删除。界面需要把这种后果差异写进动作名称与确认文案。

审核对话框也不会在提交中允许关闭。否则用户可能再次打开同一评论,无法判断前一次审核是否已经生效。锁定并不是为了限制用户,而是为了让一次不可逆决策有清楚结果。

内容长度与展示方式也影响判断

评论最长 2000 字,不能让长内容撑出屏幕。正文区域设置最大高度和内部滚动,同时使用 whitespace-pre-wrapbreak-words 保留换行并避免长链接顶破弹窗。整个对话框再限制为 85dvh,使页脚动作仍可到达。

这些布局细节和状态机同样重要。审核者如果看不全原文,或者在手机上找不到提交按钮,形式上拥有权限也无法可靠完成判断。

并发审核要接受服务端冲突

两位编辑可能同时打开同一条 reviewing 评论。前端无法靠禁用按钮阻止另一个浏览器先提交,因此弹窗里的当前状态只是一份打开时快照。提交后必须接受服务端对当前状态的再次校验。

若接口返回冲突,页面应保留审核者填写的理由,提示评论已经发生变化,并重新获取列表或上下文。直接覆盖另一位编辑的结果,会让审核记录失去可信度;把所有冲突都显示成"网络错误",又无法指导下一步。

更高并发的审核系统可以增加版本号或 updatedAt 条件更新。当前项目没有这类契约能力,所以前端不假装提供强一致锁,而是依靠写后失效和明确错误回到最新服务器事实。

小结

评论审核界面把机器状态转成通过、拒绝和待复核三个动作,并在同一决策空间里提供文章、父评论、原文和历史理由。理由字段随动作变化,成功提示明确结果,官方回复被拒绝时保留输入。

各位看官,审核页的效率不取决于一屏能塞多少行,而取决于审核者能否在一次视线范围内获得足够信息,并确信点击之后会发生什么。

延伸阅读


如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏 「成为全栈」

🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html

📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer

相关推荐
liangshanbo121515 小时前
React 状态持久化面试题——原理深挖版
react·状态持久化
liangshanbo121515 小时前
Redux 的中间件(Middleware)的工作机制
中间件·react·redux
FungLeo1 天前
成为全栈·React 管理后台篇·Markdown 编辑器:预览、暗色主题与连续图片粘贴
图片上传·react·响应式设计·markdown编辑器·异步编程·成为全栈
FungLeo2 天前
成为全栈·React 管理后台篇·表单页范式:校验、数据回填与未保存保护
react·表单设计·zod·react hook form·成为全栈·数据回填
名字还没想好☜2 天前
React 文件上传实战:预览 URL 回收、多文件逐个进度与拖拽放置区踩坑
前端·react·next.js
FungLeo2 天前
成为全栈·React 管理后台篇·列表页范式:让分页、筛选和返回位置进入 URL
react·前端架构·分页查询·react router·成为全栈·url状态
FungLeo3 天前
成为全栈·React 管理后台篇·服务端状态、会话状态、界面状态:不要都塞进 Zustand
react·状态管理·zustand·react hook form·成为全栈·tanstack query
FungLeo3 天前
成为全栈·React 管理后台篇·前端鉴权闭环:内存令牌、刷新旋转与路由守卫
react·路由守卫·zustand·刷新令牌·成为全栈·前端鉴权
余槐i4 天前
从useState到Agent状态:React状态管理为何在AI工作流中失灵?
react·状态管理·ai agent·langgraph·tool calling