跳转到内容

Ui交互规范

弹窗、表单、加载、空状态、报错反馈、页面跳转


  • Standard:弹窗分为三类,按使用场景选择:
    • Dialog(对话框):需要用户确认的操作,如删除确认、二次验证
    • Drawer(抽屉):表单填写、详情查看等内容较多的场景
    • Modal(模态弹窗):展示信息、引导等轻量交互
  • Requirement:所有弹窗必须支持点击遮罩关闭和 ESC 键关闭(含未保存变更的弹窗除外,需二次确认)。
  • Requirement:弹窗标题必须明确,操作按钮必须有明确语义(“确认删除”而非”确定”)。
  • Standard:确认类危险操作(删除、重置)使用红色主按钮,普通操作使用蓝色主按钮。
  • Standard:弹窗内容高度超过视口 70% 时,内容区域必须可滚动,按钮固定在底部。
  • Standard:弹窗内部提交按钮在请求中显示 loading 状态,且禁用关闭,避免重复提交。
  • Caution:弹窗中含未保存的表单数据时,关闭前必须弹出二次确认。
import { Modal, Button, Form, Input } from 'antd';
 
interface DeleteConfirmProps {
  open: boolean;
  title: string;
  onConfirm: () => Promise<void>;
  onCancel: () => void;
}
 
export function DeleteConfirm({ open, title, onConfirm, onCancel }: DeleteConfirmProps) {
  const [loading, setLoading] = useState(false);
 
  const handleConfirm = async () => {
    setLoading(true);
    try {
      await onConfirm();
      onCancel();
    } finally {
      setLoading(false);
    }
  };
 
  return (
    <Modal
      title="确认删除"
      open={open}
      onCancel={onCancel}
      maskClosable={!loading}
      keyboard={!loading}
      footer={[
        <Button key="cancel" onClick={onCancel} disabled={loading}>
          取消
        </Button>,
        <Button key="confirm" danger loading={loading} onClick={handleConfirm}>
          确认删除
        </Button>,
      ]}
    >
      <p>确定要删除「{title}」吗?此操作不可恢复。</p>
    </Modal>
  );
}
反模式问题建议
弹窗标题只写”提示”用户无法判断操作内容标题说明具体操作
按钮文案”确定/取消”语义模糊使用”保存/取消”或”确认删除/取消”
弹窗中嵌套弹窗层级混乱、体验差改用步骤切换或 Drawer
危险操作无二次确认误操作风险必须二次确认

  • Requirement:所有输入项必须有明确的校验规则,前端校验与后端校验保持一致。
  • Standard:必填项在 label 前加红色 * 标记。
  • Standard:校验失败时,输入框标红并在下方展示错误信息,错误信息必须具体可操作(“请输入 6-20 位密码”而非”格式错误”)。
  • Default:失焦(onBlur)时触发校验,提交时全量校验。
  • Standard:表单包含三种状态:编辑中、提交中(loading)、提交成功/失败。
  • Requirement:提交中必须禁用提交按钮,防止重复提交。
  • Standard:提交成功后展示成功提示并关闭弹窗或跳转,提交失败展示错误信息且保留用户输入。
  • Standard:数字输入框限制最大值、最小值和小数位数。
  • Standard:文本输入框设置 maxLength,并在右侧显示已输入/最大字数(10/100)。
  • Caution:手机号、身份证等敏感输入在前端做格式校验,同时后端必须二次校验。
import { Form, Input, InputNumber, Button, message } from 'antd';
 
const [form] = Form.useForm();
const [submitting, setSubmitting] = useState(false);
 
const handleSubmit = async (values: any) => {
  setSubmitting(true);
  try {
    await api.createUser(values);
    message.success('创建成功');
    onSuccess?.();
  } catch (e) {
    message.error('创建失败,请重试');
  } finally {
    setSubmitting(false);
  }
};
 
<Form form={form} layout="vertical" onFinish={handleSubmit}>
  <Form.Item
    label="用户名"
    name="name"
    rules={[
      { required: true, message: '请输入用户名' },
      { min: 2, max: 20, message: '用户名长度为 2-20 个字符' },
    ]}
  >
    <Input maxLength={20} showCount placeholder="请输入用户名" />
  </Form.Item>
 
  <Form.Item
    label="年龄"
    name="age"
    rules={[{ required: true, message: '请输入年龄' }]}
  >
    <InputNumber min={0} max={150} style={{ width: '100%' }} />
  </Form.Item>
 
  <Form.Item>
    <Button type="primary" htmlType="submit" loading={submitting}>
      保存
    </Button>
  </Form.Item>
</Form>
反模式问题建议
无前端校验直接提交用户体验差、增加服务端压力前端做完整校验
错误信息只写”格式错误”用户不知如何修正给出具体可操作的提示
提交中不禁用按钮重复提交loading 时禁用按钮
提交失败清空表单用户需重新输入保留输入内容

  • Standard:加载分为三类,按场景选择:
    • 页面级加载:首屏数据加载,使用全屏 Spin 或骨架屏
    • 区域级加载:列表、卡片等局部数据加载,在区域内显示 Spin
    • 按钮级加载:提交操作时按钮显示 loading
  • Standard:加载时间超过 300ms 才展示 loading 动画,避免闪烁。
  • Caution:加载时间超过 10s 仍未完成时,应提示用户”加载较慢,请稍后重试”并提供重试按钮。
  • Suggestion:列表、卡片等结构化内容优先使用骨架屏而非纯 Spin,减少用户等待焦虑。
  • Standard:骨架屏的布局结构必须与实际内容一致,高度相同,避免内容跳动。
import { Spin, Skeleton } from 'antd';
 
// 区域级加载
<div style={{ position: 'relative', minHeight: 200 }}>
  {loading ? <Skeleton active paragraph={{ rows: 4 }} /> : <UserList data={data} />}
</div>
 
// 按钮级加载
<Button type="primary" loading={submitting} onClick={handleSubmit}>
  提交
</Button>
反模式问题建议
所有加载都用全屏 Spin遮挡内容、体验差局部区域用局部 Spin
无 loading 状态用户不知道是否在请求所有异步操作必须有 loading
骨架屏与实际内容高度不一致内容跳动保持布局一致

  • Standard:空状态分为三类,必须分别设计:
    • 首次空状态:用户从未使用过该功能,引导用户开始使用
    • 搜索无结果:用户搜索/筛选后无匹配数据,提供清空筛选入口
    • 数据为空:功能已使用但暂无数据,提供创建入口
  • Requirement:每个列表页面必须处理空状态,禁止出现空白页面。
  • Standard:空状态包含图标 + 文案 + 操作按钮(如”新建”、“清空筛选”)。
  • Standard:空状态文案使用”暂无数据”而非”没有数据”或”null”。
import { Empty, Button } from 'antd';
 
// 首次空状态
<Empty
  image={Empty.PRESENTED_IMAGE_SIMPLE}
  description="还没有用户,点击下方按钮创建"
>
  <Button type="primary" onClick={() => navigate('/user/create')}>
    新建用户
  </Button>
</Empty>
 
// 搜索无结果
<Empty
  image={Empty.PRESENTED_IMAGE_SIMPLE}
  description="没有匹配的用户"
>
  <Button onClick={resetFilters}>清空筛选条件</Button>
</Empty>
反模式问题建议
列表为空时页面空白用户困惑展示空状态组件
空状态无操作入口用户不知下一步提供创建或清空入口
空状态文案”没有数据”表达生硬使用”暂无数据”

  • Standard:错误分为四类,按类型处理:
错误类型处理方式示例
表单校验错误输入框下方红字提示“请输入正确的手机号”
操作失败Message 提示,保留当前状态“保存失败,请重试”
页面加载失败页面级错误提示 + 重试按钮“加载失败,请重试”
网络异常全局提示 + 引导检查网络“网络异常,请检查网络连接”
  • Standard:瞬时操作结果用 message(3 秒自动消失);需要用户关注的警告用 notification(右上角);错误详情用 Modal 展示。
  • Requirement:错误提示必须包含具体原因,禁止只展示”操作失败”。
  • Requirement:必须配置全局错误边界(Error Boundary),捕获未处理的渲染异常,展示统一的错误页面。
  • Standard:接口请求统一在 axios 拦截器中处理网络错误和业务错误码,避免每个请求单独处理。
// 全局 Error Boundary
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
 
  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }
 
  componentDidCatch(error: Error, info: any) {
    console.error('页面渲染异常:', error, info);
  }
 
  render() {
    if (this.state.hasError) {
      return (
        <div style={{ padding: 48, textAlign: 'center' }}>
          <h2>页面出错了</h2>
          <p>{this.state.error?.message}</p>
          <Button onClick={() => window.location.reload()}>刷新页面</Button>
        </div>
      );
    }
    return this.props.children;
  }
}
// axios 统一错误处理
axios.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      message.error('登录已过期,请重新登录');
      // 跳转登录页
    } else if (error.response?.status >= 500) {
      message.error('服务器异常,请稍后重试');
    } else if (error.code === 'ECONNABORTED') {
      message.error('请求超时,请重试');
    } else {
      message.error(error.response?.data?.message || '请求失败');
    }
    return Promise.reject(error);
  }
);
反模式问题建议
错误提示”操作失败”用户不知原因展示具体错误原因
接口错误在每个组件单独处理重复代码统一在拦截器处理
渲染崩溃白屏用户体验极差配置 Error Boundary
401 未跳转登录用户卡在无权限页面自动跳转登录页

  • Standard:站内跳转使用路由跳转(router.push / navigate),禁止使用 window.location.href。
  • Standard:站外链接使用 <a> 标签并设置 target="_blank",同时添加 rel="noopener noreferrer"。
  • Caution:未保存的表单页面跳转前必须二次确认。
  • Standard:页面跳转时顶部展示进度条(如 NProgress),告知用户正在加载。
  • Standard:新页面加载完成后滚动条回到顶部。
import { useNavigate } from 'react-router-dom';
 
function UserList() {
  const navigate = useNavigate();
 
  const handleEdit = (id: string) => {
    navigate(`/user/edit/${id}`);
  };
 
  // 站外链接
  return (
    <a href="https://docs.example.com" target="_blank" rel="noopener noreferrer">
      查看文档
    </a>
  );
}
反模式问题建议
站内跳转用 window.location.href全页刷新、丢失状态使用路由跳转
新窗口链接无 rel="noopener"安全风险添加 rel="noopener noreferrer"
未保存表单直接跳转数据丢失跳转前二次确认