返回场景指南
场景指南 06

表单、删除与权限:把边界讲清楚

这些操作关乎后果。动效的职责是把风险和状态摆在眼前,帮助用户在关键时刻做出判断。

表单校验、删除确认和权限变更都在处理边界。动效要把风险、影响范围、进行中状态和恢复路径安排在用户做决定的位置,让人能看见后果,也能继续完成任务。

先做这个判断

先区分可撤销、需要确认和立即生效的动作,再决定反馈放在字段、对象还是全局层级。

实施路径

  1. 01

    校验贴着输入发生

    错误信息、焦点和一次短促提示围绕具体字段出现,减少来回找原因。

  2. 02

    删除先解释后果

    确认层说明将要失去什么;可恢复时在原位置给出撤销入口。

  3. 03

    权限变化标出影响范围

    成员、角色和可访问内容一起更新,避免只改一个标签却留下旧状态。

先画出风险和可恢复性的地图

把高风险交互都做成同一种弹窗,往往会让重要程度变得模糊。表单中的邮箱格式错误需要快速、贴近输入的帮助;移除团队成员需要说明对象、影响和是否可以恢复;把链接从“受邀成员”改为“任何人可访问”需要让人看见范围扩大到了哪里。先按后果而不是控件类型分组:可立即修正、可撤销、需要明确承诺、会影响他人的设置。每一组决定反馈的位置、速度和文字强度。

风险地图也能阻止过度表演。轻微的输入错误不需要整张页面震动,真正的删除确认也不该只靠红色和一次闪烁。界面应把注意力导向下一步:字段错误让焦点和帮助文本回到具体输入;删除确认让目标名称、影响数量和撤销策略同时可读;权限变更让旧规则、新规则和受影响对象并列出现。动效的节奏随风险提高而更有停顿,信息的证据也随风险提高而更完整。

四种后果从可立即修正到影响他人,分别对应字段提示、撤销、明确确认和范围摘要。风险上升可立即修正字段提示可撤销原位撤销需要承诺对象+后果影响他人范围摘要
图解一:按后果安排反馈四种后果从可立即修正到影响他人,分别对应字段提示、撤销、明确确认和范围摘要。

表单校验贴着输入发生

好的行内校验从用户行为开始,而不是从错误状态开始。用户输入、离开字段、点击提交,三个时机需要不同策略。输入过程中优先给格式和要求的前瞻提示;离开字段时可以确认完整性;提交时再集中处理遗漏项,并把焦点带到第一个需要处理的地方。这样用户不会在每敲一个字符时被打断,也不会在提交后面对一串没有顺序的红字。每条提示都应说明问题、修正方式和修正后的预期。

动效只需帮助定位。字段边界可以在 120 到 180 毫秒内改变,错误图标和说明可在原位置显现,输入光标保持可编辑。一次极短的横向提醒适合提交后的单个错误,连续晃动会让错误本身变得比修正方案更抢眼。多个错误同时出现时,用清晰的文档顺序、错误摘要和逐项焦点导航代替逐个动画。低动态路径保留边界、说明和焦点,把额外位移移除。

删除需要对象、承诺和恢复

删除流程的第一责任是确认对象。确认层里应出现项目名称、相关数量、不可恢复的内容和保留内容;当对象的名称相近时,还可补充头像、缩略图或路径。用户按下删除后,原列表里的项目可以进入处理中状态,避免看起来像点击无效。服务器确认后,再让项目离开列表,并在原位置或邻近位置留下撤销入口。整个过程讲的是同一件事,不同区域的状态要同步,而不是各自播放一段漂亮动画。

按住确认适合代价高、误触概率高的操作,因为它把承诺变成一段可见的时间。进度填充要始终贴着手指或指针的来源,松开后立即取消,不要留下模糊的半完成状态。它不能取代文本说明:用户仍要知道删掉的是谁、能否恢复、多久后彻底清除。对于可撤销的归档或移除,撤销窗口应该稳定地停留足够久,并在窗口关闭前让用户完成其他任务;不要用短暂浮层逼迫用户立刻做决定。

删除动作依次展示后果说明、处理中状态和删除结果与撤销入口。承诺前名称、影响、选择承诺中处理中、防重复承诺后结果、撤销、继续
图解二:承诺前、中、后的反馈删除动作依次展示后果说明、处理中状态和删除结果与撤销入口。

权限变化要展示范围,而非只更新标签

权限设置的困难来自不可见的受影响对象。把“仅受邀成员”改为“拥有链接的任何人”时,界面需要同时说明访问者、可见内容、链接行为和保存后的结果。选择控件附近可以立即更新解释文案,确认保存时则把最终规则放到页面状态区,并更新成员或共享列表中的相关标记。用户看到的是一条可追踪的规则变化,而不是一个孤立的下拉框选择。

权限的视觉层级应当和影响范围一致。只影响当前文档的角色调整可以在原处确认;影响整个工作区、公开链接或外部邀请的选择,应当获得更明确的总结和可回退路径。颜色可以帮助区分状态,完整句子负责说明谁会受到影响。对于实时协作产品,还要处理并发:当其他管理员同时修改规则时,页面需要说明本地选择是否仍有效、保存会覆盖什么、以及怎样重新载入最新状态。动效在这里负责让冲突和更新被看见,绝不掩盖它们。

用失败路径验收边界交互

真正的验收从失败开始。让校验接口返回延迟和错误,观察用户是否知道当前字段仍可编辑;让删除请求失败,确认对象没有提前消失且错误落在原操作附近;让权限保存发生冲突,检查页面是否保留了用户选择并给出明确下一步。再加入网络离线、键盘操作、窄屏和低动态偏好。每一种异常都验证同一个原则:风险越高,界面越要把原因、当前状态和恢复动作留在同一上下文里。

团队也应记录哪些反馈是“承诺前”、哪些是“承诺中”、哪些是“承诺后”。承诺前的提示帮助理解后果;承诺中的反馈防止重复操作;承诺后的结果负责恢复任务节奏。这个分层能让设计、产品和工程对同一个交互使用相同语言,并减少把警告、加载和成功信息堆在一个组件里的冲动。当一个页面的边界操作都能被这样描述,动效规范就已经接近可复用的产品规则。

旧规则、新规则、受影响对象和保存结果组成可比较的权限变化说明。旧规则仅受邀成员新规则拥有链接的任何人受影响对象成员、链接、可见内容保存结果状态区中的最终规
图解三:权限变化的范围证据旧规则、新规则、受影响对象和保存结果组成可比较的权限变化说明。

高风险交互发布清单

  • 确认层明确对象与影响名称、数量、范围和可恢复性同时可读。
  • 错误贴着字段并保留编辑焦点提示解释问题、修正方式和预期结果。
  • 处理中状态阻止重复承诺按钮、原对象和状态区同步说明进行中。
  • 失败和撤销入口留在原上下文用户无需重新寻找对象或回忆刚才做了什么。
  • 权限变化展示受影响范围旧规则、新规则和受影响成员或内容可被比较。

案例:删除项目后保留可恢复的任务节奏

项目列表中的删除动作需要确认对象、等待服务器、更新列表并给出撤销机会。

<section class="delete-demo">
  <ul data-project-list>
    <li class="project-row" data-project-row data-project-id="alpha" data-state="ready">
      <span>Project Alpha</span>
      <button type="button" data-delete>Delete</button>
    </li>
  </ul>
  <div data-project-undo hidden>
    <span>Project deleted.</span>
    <button type="button" data-undo>Undo</button>
  </div>
  <p role="status" aria-live="polite" data-delete-status>Project Alpha is available.</p>
</section>

<style>
.delete-demo { display: grid; gap: 12px; max-width: 28rem; }
.project-row { display: flex; justify-content: space-between; gap: 12px; align-items: center; }
@media (prefers-reduced-motion: no-preference) {
  .project-row { transition: opacity 180ms ease, transform 180ms ease; }
  .project-row[data-state="leaving"] { opacity: 0; transform: translateX(8px); }
}
</style>

<script>
const row = document.querySelector("[data-project-row]");
const list = document.querySelector("[data-project-list]");
const deleteButton = row.querySelector("[data-delete]");
const undoRegion = document.querySelector("[data-project-undo]");
const undoButton = undoRegion.querySelector("[data-undo]");
const status = document.querySelector("[data-delete-status]");
const projectId = row.dataset.projectId;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
let leaveTimer = 0;
let onLeaveEnd = null;

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

async function deleteProject(id) {
  await waitForServer();
}

async function restoreProject(id) {
  await waitForServer();
}

function clearLeaving() {
  window.clearTimeout(leaveTimer);
  leaveTimer = 0;
  if (onLeaveEnd) row.removeEventListener("transitionend", onLeaveEnd);
  onLeaveEnd = null;
}

function removeAfterExit() {
  const finishRemoval = () => {
    clearLeaving();
    if (row.isConnected) row.remove();
  };

  if (reducedMotion.matches) {
    finishRemoval();
    return;
  }

  onLeaveEnd = (event) => {
    if (event.target !== row || event.propertyName !== "opacity") return;
    finishRemoval();
  };
  row.addEventListener("transitionend", onLeaveEnd);
  leaveTimer = window.setTimeout(finishRemoval, 220);
}

async function removeProject() {
  if (deleteButton.disabled) return;

  row.dataset.state = "deleting";
  deleteButton.disabled = true;
  status.textContent = "Deleting Project Alpha.";

  try {
    await deleteProject(projectId);
    row.dataset.state = "leaving";
    undoRegion.hidden = false;
    undoButton.disabled = false;
    status.textContent = "Project deleted. Undo is available.";
    undoButton.focus();
    removeAfterExit();
  } catch {
    row.dataset.state = "ready";
    deleteButton.disabled = false;
    status.textContent = "Project deletion failed. Try again.";
    deleteButton.focus();
  }
}

async function restoreProjectRow() {
  if (undoButton.disabled) return;

  clearLeaving();
  undoButton.disabled = true;
  status.textContent = "Restoring Project Alpha.";

  try {
    await restoreProject(projectId);
    if (!row.isConnected) list.append(row);
    row.dataset.state = "ready";
    deleteButton.disabled = false;
    undoButton.disabled = false;
    undoRegion.hidden = true;
    status.textContent = "Project restored.";
    deleteButton.focus();
  } catch {
    undoButton.disabled = false;
    status.textContent = "Project restoration failed. Try again.";
    undoButton.focus();
  }
}

deleteButton.addEventListener("click", removeProject);
undoButton.addEventListener("click", restoreProjectRow);
</script>

列表项只有在服务端确认后才离开。两条路径都保留状态文字和撤销入口;默认路径额外加入极短的离开反馈,低动态路径直接更新结果。