成为全栈·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-wrap 与 break-words 保留换行并避免长链接顶破弹窗。整个对话框再限制为 85dvh,使页脚动作仍可到达。
这些布局细节和状态机同样重要。审核者如果看不全原文,或者在手机上找不到提交按钮,形式上拥有权限也无法可靠完成判断。
并发审核要接受服务端冲突
两位编辑可能同时打开同一条 reviewing 评论。前端无法靠禁用按钮阻止另一个浏览器先提交,因此弹窗里的当前状态只是一份打开时快照。提交后必须接受服务端对当前状态的再次校验。
若接口返回冲突,页面应保留审核者填写的理由,提示评论已经发生变化,并重新获取列表或上下文。直接覆盖另一位编辑的结果,会让审核记录失去可信度;把所有冲突都显示成"网络错误",又无法指导下一步。
更高并发的审核系统可以增加版本号或 updatedAt 条件更新。当前项目没有这类契约能力,所以前端不假装提供强一致锁,而是依靠写后失效和明确错误回到最新服务器事实。
小结
评论审核界面把机器状态转成通过、拒绝和待复核三个动作,并在同一决策空间里提供文章、父评论、原文和历史理由。理由字段随动作变化,成功提示明确结果,官方回复被拒绝时保留输入。
各位看官,审核页的效率不取决于一屏能塞多少行,而取决于审核者能否在一次视线范围内获得足够信息,并确信点击之后会发生什么。
延伸阅读
如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏 「成为全栈」:
🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html
📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer
