React封装过哪些组件-下拉选择器和弹窗表单

发布于:2025-07-12 ⋅ 阅读:(15) ⋅ 点赞:(0)

背景(S - Situation):在某活动管理系统中,前端页面需要支持用户选择“要配置的当前活动”,并提供「新增」「编辑」功能,操作内容包括填写活动名称、ID、版本号等字段。原始实现逻辑分散、复用性差,不利于维护和功能拓展。

目标(T - Task):

封装一个通用的 ActivitySelector 组件,支持以下功能:

  • ✅ 异步加载活动列表,支持 loading 状态;

  • ✅ 支持新增 / 编辑活动信息,并自动更新下拉框内容;

  • ✅ 支持禁用某些选项、设置默认值;

  • ✅ 弹窗使用 antd.Modal


⚠️ 实现重难点总结

1. 组件对外暴露弹窗控制器(Context 模式)

难点在于:如何在父组件中控制子组件内部的 Modal 行为

  • 采用 React 的 createContext + useContext 创建全局控制器;

  • 内部封装了 openModal() 方法,供外部调用;

  • 父组件通过 useActivityModal() 获取控制器,实现跨组件通信;

  • 解耦了弹窗的触发逻辑,使组件更灵活、可扩展。

👉 适用于复杂业务流程、URL 参数触发弹窗、全局快捷操作等场景。


2. 异步数据加载与状态同步

  • 初始加载通过 fetchOptions 动态获取活动列表;

  • 新增或编辑成功后自动刷新并回填选项;

  • 异步处理流程中加入 loading 状态,保证用户体验;

  • 使用 Form.setFieldsValue() 动态填充表单数据。


3. 新增/编辑共用同一个 Modal + 表单

  • 通过 editItem 区分是“新增”还是“编辑”状态;

  • 弹窗标题、确认逻辑复用,简化了 UI;

  • 校验规则、默认值、字段配置均可灵活拓展。


4. 选项禁用处理 + 名称唯一性校验

  • 支持传入 disabledIds 数组动态禁用某些选项;

  • 在表单提交时手动校验名称重复,防止业务错误;

  • 校验逻辑抽离出来便于维护或拓展为服务端验证。


5. 默认版本号处理

  • 若用户未填写版本号,自动填充为指定 defaultVersion

  • 避免每次用户手动输入,提高使用体验。


当然可以,咱来详细解释一下这句:


✅ 「对外暴露 context 控制器,支持父组件主动打开弹窗」是什么意思?


🔧 背景场景

我们在封装一个组件(比如 <ActivitySelector />)时,通常 弹窗的打开/关闭是组件内部控制的,比如用户点击“新增”或“编辑”按钮时,组件内部去 setModalVisible(true) 打开弹窗。

但有时候你会希望 在组件外部 主动打开弹窗,例如:

  • 有一个按钮在父组件中,点击它时希望直接打开弹窗(比如预设一个新活动);

  • 希望根据某个外部逻辑(例如 URL 参数)控制弹窗的显示;

  • 在表单联动或流程中,用户完成前一步操作后触发弹窗。


📦 如何实现?

这就需要组件对外暴露一个控制器(Controller),最常见的方式就是使用 React 的 Context + useContext + Provider


✅ 具体做法举例(摘自你项目中的代码)
// 创建一个 context(上下文对象)
const ActivityModalContext = createContext(null);

// 暴露一个 Hook,让外部能使用这个控制器
export const useActivityModal = () => useContext(ActivityModalContext);

然后在组件内部:

<ActivityModalContext.Provider value={{ openModal }}>
  {/* 组件内容 */}
</ActivityModalContext.Provider>

这样父组件就可以写成这样:

import { useActivityModal } from './ActivitySelector';

function ParentComponent() {
  const { openModal } = useActivityModal();

  return <Button onClick={() => openModal({ id: 'xxx' })}>外部打开弹窗</Button>;
}

✅ 总结:这是什么意思?

「对外暴露 context 控制器」就是指:

封装组件时,借助 React 的 Context 机制,把内部的控制方法(如打开弹窗)暴露出去,让外部组件也能调用它。


🚀 好处

好处 说明
📦 解耦 父组件无需知道 Modal 是怎么实现的,只要能打开它就行
🧩 灵活 可以在任何地方调用 openModal,比如 URL 路由、定时器、其他组件事件等
👨‍💻 易于测试与复用 可以单独测试控制器逻辑,也可以跨页面/组件共享


ActivitySelector.tsx 完整封装代码

import React, { useState, useEffect, createContext, useContext } from 'react';
import { Select, Modal, Form, Input, Button, message } from 'antd';

const { Option } = Select;

// --------- Context 控制器 ---------
const ActivityModalContext = createContext(null);

export const useActivityModal = () => useContext(ActivityModalContext);

// --------- 主组件封装 ---------
const ActivitySelector = ({
  value,
  onChange,
  fetchOptions,
  onCreate,
  onEdit,
  disabledIds = [],
  defaultVersion = 'v1.0',
}) => {
  const [options, setOptions] = useState([]);
  const [loading, setLoading] = useState(false);
  const [modalVisible, setModalVisible] = useState(false);
  const [editItem, setEditItem] = useState(null);
  const [form] = Form.useForm();

  // 初始化加载
  useEffect(() => {
    loadOptions();
  }, []);

  const loadOptions = async () => {
    setLoading(true);
    const data = await fetchOptions?.();
    setOptions(data || []);
    setLoading(false);
  };

  const openModal = (item = null) => {
    setEditItem(item);
    form.setFieldsValue(item || { version: defaultVersion });
    setModalVisible(true);
  };

  const handleOk = async () => {
    try {
      const formData = await form.validateFields();

      // 校验重复名称(或其他字段)
      const isRepeat = options.some(
        (item) => item.name === formData.name && item.id !== editItem?.id
      );
      if (isRepeat) {
        message.error('活动名称重复,请重新输入');
        return;
      }

      const result = editItem
        ? await onEdit?.(editItem.id, formData)
        : await onCreate?.(formData);

      await loadOptions();
      onChange?.(result); // 自动选中新项
      setModalVisible(false);
    } catch (err) {
      console.error('表单提交失败', err);
    }
  };

  return (
    <ActivityModalContext.Provider value={{ openModal }}>
      <div style={{ display: 'flex', gap: 8 }}>
        <Select
          value={value?.id}
          onChange={(val) => {
            const selected = options.find((item) => item.id === val);
            onChange?.(selected);
          }}
          loading={loading}
          style={{ flex: 1 }}
          placeholder="请选择活动"
          allowClear
          showSearch
          optionFilterProp="children"
        >
          {options.map((item) => (
            <Option key={item.id} value={item.id} disabled={disabledIds.includes(item.id)}>
              {item.name}({item.version})
            </Option>
          ))}
        </Select>

        <Button onClick={() => openModal()}>新增</Button>
        <Button onClick={() => openModal(value)} disabled={!value}>
          编辑
        </Button>
      </div>

      <Modal
        title={editItem ? '编辑活动' : '新增活动'}
        open={modalVisible}
        onCancel={() => setModalVisible(false)}
        onOk={handleOk}
        destroyOnClose
      >
        <Form form={form} layout="vertical" preserve={false}>
          <Form.Item
            label="活动名称"
            name="name"
            rules={[{ required: true, message: '请输入活动名称' }]}
          >
            <Input />
          </Form.Item>

          <Form.Item
            label="活动 ID"
            name="id"
            rules={[{ required: true, message: '请输入活动 ID' }]}
          >
            <Input />
          </Form.Item>

          <Form.Item
            label="版本号"
            name="version"
            rules={[{ required: true, message: '请输入版本号' }]}
          >
            <Input placeholder="如 v1.0" />
          </Form.Item>

          <Form.Item label="扩展信息" name="meta">
            <Input.TextArea placeholder="可以是 JSON、备注等" />
          </Form.Item>
        </Form>
      </Modal>
    </ActivityModalContext.Provider>
  );
};

export default ActivitySelector;

🧪 外部调用方式(使用 Context 控制器)

import React, { useState, useEffect } from 'react';
import ActivitySelector, { useActivityModal } from './ActivitySelector';

const ParentPage = () => {
  const [selected, setSelected] = useState(null);
  const { openModal } = useActivityModal();

  const fetchOptions = async () => {
    return [
      { id: 'act001', name: '暑期促销', version: 'v1.0', meta: '' },
      { id: 'act002', name: '双11预热', version: 'v1.2', meta: '' },
    ];
  };

  const handleCreate = async (data) => {
    // 模拟添加后返回新数据项
    return { id: 'act003', ...data };
  };

  const handleEdit = async (id, data) => {
    return { id, ...data };
  };

  return (
    <div>
      <h2>活动管理</h2>
      <ActivitySelector
        value={selected}
        onChange={setSelected}
        fetchOptions={fetchOptions}
        onCreate={handleCreate}
        onEdit={handleEdit}
        disabledIds={['act002']}
        defaultVersion="v2.0"
      />

      <Button onClick={() => openModal()}>🔧 外部控制打开弹窗</Button>
    </div>
  );
};

export default ParentPage;

如你有后续需求(如:分页、远程搜索、多选、自定义弹窗样式或 MUI 替换),可以继续在这个封装基础上扩展,我也可以帮你一步步完成。需要我继续用 STAR 法则 补充笔记内容吗?


网站公告

今日签到

点亮在社区的每一天
去签到