当前位置: 首页 > news >正文

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

背景(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 }}><Selectvalue={value?.id}onChange={(val) => {const selected = options.find((item) => item.id === val);onChange?.(selected);}}loading={loading}style={{ flex: 1 }}placeholder="请选择活动"allowClearshowSearchoptionFilterProp="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><Modaltitle={editItem ? '编辑活动' : '新增活动'}open={modalVisible}onCancel={() => setModalVisible(false)}onOk={handleOk}destroyOnClose><Form form={form} layout="vertical" preserve={false}><Form.Itemlabel="活动名称"name="name"rules={[{ required: true, message: '请输入活动名称' }]}><Input /></Form.Item><Form.Itemlabel="活动 ID"name="id"rules={[{ required: true, message: '请输入活动 ID' }]}><Input /></Form.Item><Form.Itemlabel="版本号"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><ActivitySelectorvalue={selected}onChange={setSelected}fetchOptions={fetchOptions}onCreate={handleCreate}onEdit={handleEdit}disabledIds={['act002']}defaultVersion="v2.0"/><Button onClick={() => openModal()}>🔧 外部控制打开弹窗</Button></div>);
};export default ParentPage;

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

http://www.dtcms.com/a/272453.html

相关文章:

  • 常用的.gitconfig 配置
  • 【显示模块】嵌入式显示与触摸屏技术理论
  • HarmonyOS AI辅助编程工具(CodeGenie)UI生成
  • 时序数据库的存储之道:从数据特性看技术要点
  • 使用深度学习框架yolov8训练监控视角下非机动车电动车头盔佩戴检测数据集VOC+YOLO格式11999张4类别步骤和流程
  • UEditor 对接 秀米 手机编辑器流程与问题
  • ClickHouse 查看正在执行的SQL查询
  • Django--01基本请求与响应流程
  • go go go 出发咯 - go web开发入门系列(四) 数据库ORM框架集成与解读
  • selenium跳转到新页面时如何进行定位
  • 前缀和|差分
  • S7-1200 与 S7-300 PNS7-400 PN UDP 通信 TIA 相同项目
  • 缓存一致性问题(Cache Coherence Problem)是什么?
  • 使用Word/Excel管理需求的10个痛点及解决方案Perforce ALM
  • Word中字号与公式字体磅值(pt)的对应关系
  • 【AI智能体】智能音视频-通过关键词打断语音对话
  • RuoYi-Cloud ruoyi-gateway 网关模块
  • 海外盲盒系统:技术如何重构“信任经济”?
  • LLM 微调:从数据到部署的全流程实践与经验分享
  • 前端开发资源压缩与请求优化
  • FFmpeg滤镜相关的重要结构体
  • mongodbcdc脚本开发
  • 书生大模型实战营——1. 大语言模型原理与书生大模型提示词工程实践
  • 大数据学习7:Azkaban调度器
  • 记一次Android Studio编译报错:Execution failed for task ‘:app:compileDebugAidl‘
  • Redis数据类型之hash
  • Android 网络开发核心知识点
  • ICML 2025|快手提出了基于残差的超低码率图像压缩方法ResULIC
  • 【Bluedroid】蓝牙协议栈控制器能力解析与核心功能配置机制(decode_controller_support)
  • git中的fork指令解释