返回场景指南
场景指南 01

保存、提交和发布:让结果立刻可见

保存动作通常很短,发布可能要等几秒。两种场景都需要先给出输入已被接收的信号,再用状态变化交代结果。

保存、提交和发布表面上都像一个按钮,实际承载着用户对结果、风险和等待的判断。完整反馈会让人清楚地知道:输入已经收到,系统正在处理,结果落在何处,出错时还能怎样继续。

先做这个判断

先判断操作是否可撤销、等待是否超过一秒,以及成功后界面会发生什么变化。

实施路径

  1. 01

    点击先有回应

    按下时给出短促反馈,同时锁定重复提交的入口。

  2. 02

    等待要说清楚

    超过瞬时完成的操作,展示进行中的状态和可理解的文案。

  3. 03

    结果落在原位置

    成功、失败或可撤销入口都放在用户刚刚操作的位置附近。

先分清三种等待

一次保存可能在 150ms 内完成,用户只需要知道按下已经被接收;一次提交可能需要校验和网络往返;发布、上传和支付则常常跨过数秒。三种等待的界面语言不同。瞬时操作适合短促的按下反馈和状态替换;短等待需要把按钮从“可点”转成“进行中”;长等待要给出进度、可离开页面的说明,或至少告诉用户结果会在何处出现。把所有操作都处理成旋转图标,会把这些差异抹平。

动效时长应当跟着系统事实走。按钮的按压反馈可以在 80–140ms 内结束,让输入与画面贴在一起;真正开始请求后,再把文案切成“保存中”或“正在发布”。如果请求已经结束,成功状态不必额外停留太久,通常 600–1200ms 足够让人确认,然后让界面回到可继续工作的状态。等待的核心是节奏:用户需要一条连续、可追踪的因果链。

可发布、发布中、已发布和需要处理四个状态,其中发布中分别通向成功和失败。点击请求完成请求失败修改或重试可发布按钮可用,说明风发布中禁用重复提交,显示进度已发布展示链接与下一步需要处理保留草稿与重试入
图解一:一次发布的状态流可发布、发布中、已发布和需要处理四个状态,其中发布中分别通向成功和失败。

让状态留在动作发生处

用户点击“保存”后,最自然的视线仍停在编辑器底部、表单末尾或工具栏里。成功提示优先在这一带出现:按钮文案变为“已保存”、旁边出现简短的状态、字段边界回到稳定颜色。全局 Toast 适合补充信息,例如“所有更改已同步”,却不应承担唯一的确认职责。提示离得越远,用户越要在页面上重新寻找刚刚发生的事。

原位置反馈也让失败更容易理解。网络错误可以让按钮恢复可点,同时保留“重试”入口;字段校验可以把焦点带回具体输入,用户可直接在原处修正;发布失败则应保留草稿和失败原因。这里的连续性比华丽更重要:同一个对象从可用、进行中到结果的变化,能让用户看见系统在处理自己的请求。crossfade、轻微透明度变化和稳定的布局足以承载这一层意义。

把失败和重复操作纳入流程

反馈流程从来不只有成功路径。用户可能连续点击、请求超时、权限在提交时变化,或者离线后恢复网络。每一种情况都需要一个可预期的界面落点。进行中的主按钮应禁用重复提交,旁边保留取消或返回的路径;超时不要无声地把按钮变回原样,应该说明请求是否仍在处理;权限变化要提示新状态,并避免用户误以为内容已经成功发布。把这些边缘状态列在设计稿里,开发时就更容易写出一致的状态机。

可撤销操作值得单独安排。删除、归档和取消邀请常常会先从列表中离开,再在原位置附近给出“撤销”。这个动作可以有一小段离开动效,却要保留稳定的占位或计数变化,避免列表突然塌陷让用户以为点错了别处。撤销窗口结束后再完成最终移除。这样既能给人回头的机会,也能让列表的空间关系保持清楚。

发布按钮、同一区域的结果卡片和全局通知三层从近到远排列。同一视线补充上下文按钮状态第一确认层结果卡片链接、撤销、失败原因全局通知同步、跨页面提示
图解二:反馈离操作多近发布按钮、同一区域的结果卡片和全局通知三层从近到远排列。

发布后的页面也要接住用户

发布结束后,界面常常只显示“成功”,然后把用户留在一个已经失去下一步的页面。更完整的设计会明确接下来能做什么:打开已发布页面、复制链接、继续编辑、查看审核状态,或回到内容列表。动效可以帮助把注意力从发布按钮带到下一步入口,例如让结果卡片在原区域出现,让复制链接按钮随后可见。动作顺序要短,目标要近,用户才能顺着流程走下去。

真实产品瞬间比单一原子动效更有价值,原因就在这里。一个“发布成功”瞬间往往包含按下反馈、按钮状态、进度、结果卡片和下一步入口;它们共享同一套节奏和层级。先用 Pack 看完整状态流,再回到 press feedback、crossfade 和 duration 调整局部细节,会比从零拼几个漂亮动画更可靠。动效在这里服务的是信任感:用户能看懂,也能继续行动。

把反馈写成可验收的状态契约

每个流程都值得拥有一张状态表:idle、working、complete、failed、undoable 分别显示什么文字、哪些控件可用、焦点在哪里、是否允许离开页面。动效附着在状态转换上,而不替代状态本身。这样做能让产品、设计和开发讨论同一件事,也让测试能够检查成功、失败、重试和撤销是否真实成立。数据状态是事实,动画只是让事实更容易被看见。

这份契约也需要覆盖减弱动效。默认路径可以让按钮轻微缩放、结果卡片淡入;低动态路径保留同样的“保存中”“已保存”、同样的按钮禁用和同样的下一步入口,只缩短或取消装饰性位移。无论用户使用键盘、触屏,还是开启系统减少动效偏好,核心反馈链都应该完整。把这一点作为发布前检查,动效才会真正成为产品可靠性的一部分。

时间线从按下、输入已接收、请求期间到完成后的下一步。0ms按下120ms输入已接收请求期间发布中或进度完成后800ms结果与下一步
图解三:从按下到结果的节奏时间线从按下、输入已接收、请求期间到完成后的下一步。

保存、提交与发布验收清单

  • 点击后立刻有输入已被接收的信号按压、文案或状态色应在约 140ms 内发生变化。
  • 进行中状态阻止重复提交保留必要的取消、返回或说明路径。
  • 结果和失败靠近原动作按钮、局部状态和全局通知各自承担清楚职责。
  • 超时、失败和撤销都有明确落点草稿、错误原因和重试入口可被用户找到。
  • 完成后能继续下一步发布后提供打开、复制、继续编辑或返回列表等路径。

案例:文章发布在同一块界面内完成交接

发布按钮先进入进行中状态,成功后替换为结果卡片和打开文章入口。

<section class="publish-action" data-state="ready">
  <button class="publish-button" type="button" data-publish aria-describedby="publish-status">Publish article</button>
  <p id="publish-status" role="status" aria-live="polite" data-publish-status>Ready to publish.</p>
  <p>Requests sent: <output data-publish-submissions>0</output></p>
  <div class="publish-result" data-publish-result aria-hidden="true" inert>
    <strong>Article published.</strong>
    <a href="#published-article">Open published article</a>
  </div>
</section>

<style>
.publish-action { display: grid; gap: 12px; max-width: 28rem; }
.publish-button, .publish-result { transition: opacity 180ms ease-out, transform 180ms ease-out; }
.publish-result { opacity: 0; visibility: hidden; transform: translateY(6px); pointer-events: none; }
.publish-action[data-state="working"] .publish-button { opacity: .72; cursor: progress; }
.publish-action[data-state="complete"] .publish-button { opacity: 0; visibility: hidden; transform: scale(.98); pointer-events: none; }
.publish-action[data-state="complete"] .publish-result { opacity: 1; visibility: visible; transform: translateY(0); pointer-events: auto; }
@media (prefers-reduced-motion: reduce) {
  .publish-button, .publish-result { transition-duration: 1ms; }
}
</style>

<script>
const action = document.querySelector(".publish-action");
const publishButton = action.querySelector("[data-publish]");
const status = action.querySelector("[data-publish-status]");
const publishResult = action.querySelector("[data-publish-result]");
const publishedLink = publishResult.querySelector("a");
const requestCount = action.querySelector("[data-publish-submissions]");
let submissions = 0;

function publishRequest() {
  return new Promise((resolve) => window.setTimeout(resolve, 160));
}

async function publishArticle() {
  if (action.dataset.state !== "ready") return;

  action.dataset.state = "working";
  publishButton.disabled = true;
  status.textContent = "Publishing article.";
  submissions += 1;
  requestCount.textContent = String(submissions);

  try {
    await publishRequest();
    action.dataset.state = "complete";
    publishResult.inert = false;
    publishResult.setAttribute("aria-hidden", "false");
    status.textContent = "Article published. You can open it now.";
    publishedLink.focus();
  } catch {
    action.dataset.state = "ready";
    publishButton.disabled = false;
    status.textContent = "Publishing failed. Try again.";
  }
}

publishButton.addEventListener("click", publishArticle);
</script>

状态写在父元素的 data-state 上,按钮和结果卡片共享同一个事实,不需要依赖延时器猜测请求何时结束。结果出现前和旧按钮完成后都用 visibility 退出焦点顺序,完成状态只暴露可继续操作的结果;透明度和轻微位移负责交接,减弱动效偏好下立即完成交接。