背景(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.pswp.cn/pingmian/88590.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/88590.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/88590.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多租户架构下的多线程处理实践指南

在现代 SaaS 系统中&#xff0c;多租户架构&#xff08;Multi-Tenant Architecture&#xff09;已成为主流。然而&#xff0c;随着系统性能要求的提升和业务复杂度的增加&#xff0c;多线程成为不可避免的技术手段。但在多租户环境下使用多线程&#xff0c;容易引发数据错乱、租…

MyBatis插件机制揭秘:从拦截器开发到分页插件实战

一、拦截器体系架构解析 1.1 责任链模式在MyBatis中的实现 MyBatis通过动态代理技术构建拦截器链&#xff0c;每个插件相当于一个切面&#xff1a; // 拦截器链构建过程 public class InterceptorChain {private final List<Interceptor> interceptors new ArrayList<…

百度文心一言开源ERNIE-4.5深度测评报告:技术架构解读与性能对比

目录一、技术架构解读1.1、ERNIE 4.5 系列模型概览1.2、模型架构解读1.2.1、异构MoE&#xff08;Heterogeneous MoE&#xff09;1.2.2、视觉编码器&#xff08;Vision Encoder&#xff09;1.2.3、适配器&#xff08;Adapter&#xff09;1.2.4、多模态位置嵌入&#xff08;Multi…

Matplotlib 模块入门

Python 中有个非常实用的可视化库 ——Matplotlib。数据可视化是数据分析中不可或缺的环节&#xff0c;而 Matplotlib 作为 Python 的 2D 绘图库&#xff0c;能帮助我们生成高质量的图表&#xff0c;让数据更直观、更有说服力。接下来&#xff0c;我们将从 Matplotlib 的概述、…

LeetCode 3169.无需开会的工作日:排序+一次遍历——不需要正难则反,因为正着根本不难

【LetMeFly】3169.无需开会的工作日&#xff1a;排序一次遍历——不需要正难则反&#xff0c;因为正着根本不难 力扣题目链接&#xff1a;https://leetcode.cn/problems/count-days-without-meetings/ 给你一个正整数 days&#xff0c;表示员工可工作的总天数&#xff08;从第…

VUE3 el-table 主子表 显示

在Vue 3中&#xff0c;实现主子表&#xff08;主从表&#xff09;的显示通常涉及到两个组件&#xff1a;一个是主表&#xff08;Master Table&#xff09;&#xff0c;另一个是子表&#xff08;Detail Table&#xff09;。我们可以使用el-table组件来实现这一功能。这里&#x…

张量数值计算

一.前言前面我们介绍了一下pytorch还有张量的创建&#xff0c;而本章节我们就来介绍一下张量的计算&#xff0c;类型转换以及操作&#xff0c;这个是十分重要的&#xff0c;我们的学习目标是&#xff1a;掌握张量基本运算、掌握阿达玛积、点积运算 掌握PyTorch指定运算设备。Py…

部署项目频繁掉线-----Java 进程在云服务器内存不足被 OOM Killer 频繁杀死-----如何解决?

一、查询系统日志grep -i "java" /var/log/messages执行这条命令&#xff0c;检查系统日志里是否有 Java 进程被 OOM Killer 杀死的记录。日志中反复出现以下内容&#xff1a;Out of memory: Killed process 3679325 (java) total-vm:2947000kB, anon-rss:406604kB..…

【保姆级教程】基于anji-plus-captcha实现行为验证码(滑动拼图+点选文字),前后端完整代码奉上!

前言 验证码作为Web应用的第一道安全防线&#xff0c;其重要性不言而喻。但你是否还在为以下问题烦恼&#xff1a; 传统字符验证码用户体验差&#xff0c;识别率低&#xff1f;验证码安全性不足&#xff0c;轻易被爬虫破解&#xff1f;前后端对接繁琐&#xff0c;集成效率低&…

HTML-八股

1、DOM和BOM DOM是表示HTML或者XML文档的标准的对象模型&#xff0c;将文档中每个组件&#xff08;元素、属性等&#xff09;都作为一个对象&#xff0c;使用JS来操作这个对象&#xff0c;从而动态改变页面内容&#xff0c;结合等。 DOM是以树型结构组织文档内容&#xff0c;树…

ADI的EV-21569-SOM核心板和主板转接卡的链接说明

ADI提供给客户很多DSP的核心板&#xff0c;比如EV-21569-SOM&#xff0c;EV-21593-SOM&#xff0c;EV-SC594-SOM等&#xff0c;非常多&#xff0c;但是没有底板&#xff0c;光一个核心板怎么用呢&#xff1f;于是我就在想&#xff0c;我的21569评估板就有通用底板&#xff0c;能…

基于 Redisson 实现分布式系统下的接口限流

在高并发场景下&#xff0c;接口限流是保障系统稳定性的重要手段。常见的限流算法有漏桶算法、令牌桶算法等&#xff0c;而单机模式的限流方案在分布式集群环境下往往失效。本文将介绍如何利用 Redisson 结合 Redis 实现分布式环境下的接口限流&#xff0c;确保集群中所有节点的…

ubuntu播放rosbag包(可鼠标交互)

1 前言 众所周知&#xff0c;ubuntu中播放bag包最主要的工具是rviz&#xff0c;然而rviz有一个无法忍受的缺陷就是不支持鼠标回滚&#xff0c;并且显示的时间的ros时间&#xff0c;不是世界时间&#xff0c;因此在遇到相关bug时不能与对应的世界时间对应。基于以上&#xff0c…

一文理解缓存的本质:分层架构、原理对比与实战精粹

&#x1f4d6; 推荐阅读&#xff1a;《Yocto项目实战教程:高效定制嵌入式Linux系统》 &#x1f3a5; 更多学习视频请关注 B 站&#xff1a;嵌入式Jerry 一文理解缓存的本质&#xff1a;分层架构、原理对比与实战精粹 “缓存让系统飞起来”——但每一层缓存有何不同&#xff1f;…

【离线数仓项目】——电商域DIM层开发实战

摘要本文主要介绍了电商域离线数仓项目中DIM层的开发实战。首先阐述了DIM层的简介、作用、设计特征、典型维度分类以及交易支付场景下的表示例和客户维度表设计。接着介绍了DIM层设计规范&#xff0c;包括表结构设计规范、数据处理规范以及常见要求规范。然后详细讲解了DIM层的…

Unreal Engine 自动设置图像

void UYtGameSettingSubsystem::RunHardwareBenchmark(int32 WorkScale, float CPUMultiplier, float GPUMultiplier) {UGameUserSettings* UserSettings UGameUserSettings::GetGameUserSettings();if (UserSettings){// 运行基准测试&#xff08;异步操作&#xff0c;可能需…

使用Spring Boot和PageHelper实现数据分页

在Spring Boot项目中&#xff0c;利用PageHelper插件可以轻松实现数据分页功能。以下是具体的实现步骤和代码示例。添加依赖在项目的pom.xml文件中添加PageHelper和MyBatis的依赖。<dependency><groupId>com.github.pagehelper</groupId><artifactId>p…

【IT-Infra】从ITIL到CMDB,配置管理,资产管理,物理机与设备管理(含Infra系列说明)

【IT-Infra】从ITIL到CMDB&#xff0c;配置管理&#xff0c;资产管理&#xff0c;物理机与设备管理&#xff08;含Infra系列说明&#xff09; 文章目录序&#xff1a;Infra系列说明1、ITIL 信息技术基础架构库&#xff08;起源&#xff09;2、CMDB 配置管理数据库&#xff08;I…

vue使用printJS实现批量打印及单个打印 避免空白页

本文介绍了使用print-js库实现批量打印功能的实现方法。通过安装print-js依赖后,创建一个batchPrintAction方法,该方法接收选中行数据,生成包含多个标签页的HTML字符串。每个标签页以表格形式展示6个数据字段,并设置了80mm50mm的标签尺寸。方法使用PrintJS进行打印,配置了…

C++ 选择排序、冒泡排序、插入排序

选择排序&#xff1a;是一种简单直观的排序算法&#xff0c;每次均是选择最小&#xff08;大&#xff09;的元素进行排序。选择排序算法思想&#xff1a;1 在未排序序列中找到最小&#xff08;大&#xff09;元素&#xff0c;存放到排序序列的起始位置2 再从剩余未排序元素中继…