如何在多个React组件中复用模态框与搜索功能以减少代码冗余
复用标签页搜索与Modal功能的实战方案
1. 抽离公共基础组件
把搜索栏、Modal这类和业务数据无关的通用功能拆成独立组件,只通过props传递个性化参数:
- 搜索组件:负责输入交互、关键词双向绑定,接收占位符、搜索触发回调等props。
- Modal组件:封装弹窗的基础样式、显示/隐藏逻辑,通过插槽传递不同标签页的弹窗内容。
- 表格容器组件:统一表格渲染逻辑,接收数据集、列配置、行点击回调等props。
示例代码(以Vue为例)
搜索组件 SearchBar.vue
<template> <div class="search-bar"> <input v-model="localKeyword" @input="triggerSearch" :placeholder="placeholder" class="search-input" /> </div> </template> <script setup> import { ref, watch } from 'vue' const props = defineProps({ modelValue: String, placeholder: { type: String, default: '输入关键词搜索' } }) const emit = defineEmits(['update:modelValue', 'search']) const localKeyword = ref(props.modelValue) watch(localKeyword, val => emit('update:modelValue', val)) const triggerSearch = () => emit('search', localKeyword.value) </script> <style scoped> .search-bar { margin-bottom: 16px; } .search-input { padding: 8px 12px; width: 100%; border-radius: 4px; border: 1px solid #ddd; } </style>
Modal组件 BaseModal.vue
<template> <div class="modal-mask" v-if="visible" @click.self="close"> <div class="modal-box"> <div class="modal-header"> <span>{{ title }}</span> <button class="close-btn" @click="close">×</button> </div> <div class="modal-body"> <slot></slot> </div> </div> </div> </template> <script setup> const props = defineProps({ visible: Boolean, title: String }) const emit = defineEmits(['close']) const close = () => emit('close') </script> <style scoped> .modal-mask { position: fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); display:flex; align-items:center; justify-content:center; } .modal-box { background:#fff; border-radius:8px; width:80%; max-width:500px; } .modal-header { padding:16px; border-bottom:1px solid #eee; display:flex; justify-content:space-between; align-items:center; } .close-btn { border:none; background:transparent; font-size:20px; cursor:pointer; } .modal-body { padding:16px; } </style>
2. 封装标签页内容组合组件
创建一个TabContent.vue,把搜索、表格、Modal组合起来,接收每个标签页独有的数据集、列配置和名称:
<template> <div class="tab-content"> <SearchBar v-model="searchKey" @search="filterData" :placeholder="`搜索${tabTitle}数据`" /> <table class="data-table"> <thead> <tr> <th v-for="col in columns" :key="col.key">{{ col.label }}</th> </tr> </thead> <tbody> <tr v-for="row in displayedData" :key="row.id" @click="openModal(row)"> <td v-for="col in columns" :key="col.key">{{ row[col.key] }}</td> </tr> </tbody> </table> <BaseModal :visible="modalShow" :title="`${tabTitle}详情`" @close="modalShow = false" > <div v-if="selectedRow"> <p v-for="col in columns" :key="col.key"> <span class="label">{{ col.label }}:</span> <span>{{ selectedRow[col.key] }}</span> </p> </div> </BaseModal> </div> </template> <script setup> import { ref, computed } from 'vue' import SearchBar from './SearchBar.vue' import BaseModal from './BaseModal.vue' const props = defineProps({ rawData: Array, columns: Array, tabTitle: String }) const searchKey = ref('') const modalShow = ref(false) const selectedRow = ref(null) const displayedData = computed(() => { if (!searchKey.value) return props.rawData return props.rawData.filter(row => { // 根据实际字段调整匹配逻辑,这里示例匹配所有字符串类型字段 return Object.values(row).some(val => typeof val === 'string' && val.includes(searchKey.value) ) }) }) const filterData = () => {} // 如需额外搜索逻辑可扩展 const openModal = (row) => { selectedRow.value = row modalShow.value = true } </script> <style scoped> .data-table { width:100%; border-collapse:collapse; } .data-table th, .data-table td { border:1px solid #eee; padding:8px; text-align:left; } .data-table th { background:#f5f5f5; } .label { font-weight:600; margin-right:8px; } </style>
3. 主页面中复用组合组件
在标签页容器里,只需要给每个标签页传入对应的数据集、列配置和标题即可:
<template> <div class="tabs-wrap"> <div class="tab-nav"> <button v-for="(tab, idx) in tabs" :key="idx" :class="{ active: activeTab === idx }" @click="activeTab = idx" > {{ tab.title }} </button> </div> <div class="tab-panels"> <TabContent v-for="(tab, idx) in tabs" :key="idx" v-show="activeTab === idx" :raw-data="tab.data" :columns="tab.columns" :tab-title="tab.title" /> </div> </div> </template> <script setup> import { ref } from 'vue' import TabContent from './TabContent.vue' // 模拟四个标签页的配置数据 const tabs = ref([ { title: '用户数据', data: [{id:1, name:'张三', age:25}, {id:2, name:'李四', age:30}], columns: [{key:'id', label:'ID'}, {key:'name', label:'姓名'}, {key:'age', label:'年龄'}] }, { title: '订单数据', data: [{id:101, orderNo:'OD2024001', amount:199}, {id:102, orderNo:'OD2024002', amount:399}], columns: [{key:'id', label:'序号'}, {key:'orderNo', label:'订单号'}, {key:'amount', label:'金额'}] }, { title: '商品数据', data: [{id:201, goodsName:'手机', price:2999}, {id:202, goodsName:'耳机', price:399}], columns: [{key:'id', label:'ID'}, {key:'goodsName', label:'商品名'}, {key:'price', label:'价格'}] }, { title: '日志数据', data: [{id:301, operator:'admin', time:'2024-05-01'}, {id:302, operator:'user', time:'2024-05-02'}], columns: [{key:'id', label:'ID'}, {key:'operator', label:'操作人'}, {key:'time', label:'操作时间'}] } ]) const activeTab = ref(0) </script> <style scoped> .tabs-wrap { max-width:1200px; margin:0 auto; } .tab-nav { margin-bottom:16px; border-bottom:1px solid #eee; } .tab-nav button { border:none; background:transparent; padding:8px 16px; cursor:pointer; margin-right:8px; } .tab-nav button.active { border-bottom:2px solid #1677ff; color:#1677ff; } </style>
4. 额外优化建议
- 复杂搜索逻辑(比如防抖、多字段精准匹配)可以抽成独立工具函数,在组件中引入复用。
- 如果多个标签页需要共享状态(比如搜索历史),可以用状态管理工具(如Pinia)集中维护。
- 表格样式、间距等公共样式可以放到全局CSS文件,避免重复编写。
内容的提问来源于stack exchange,提问作者Prajul Aaditya
相关产品推荐
相关产品推荐

