第五节:搜索组件封装剖析、弹框的使用、页面各组件联调
一. 搜索框组件封装
1. 核心思路
(1). 搜索框分为两部分:按钮区域 和 表单内容区域
(2). 按钮区域:
搜索和重置两个按钮为默认按钮,通过emits('update:xxx', xxx);对外暴露,调用父组件的更新方法。
然后声明一块 <slot name="myOwnArea"> </slot> 具名插槽区域,用于父组件自己扩展按钮
(3). 表单内容区域:
A. activeName: 在父组件中直接 :activeName=“xxx”,进行传递,主要用于搜索框展开默认显示的区域名称
B. 表单域的三个基本属性,父组件通过v-bind绑定searchFormConfig进行传递,分别是:
isInline:是否是行内显示、label-position:表单域标签的位置、label-width:表单中form-item中对应标题的宽度
C. 表单数据对象 :model="modelValue", 父组件通过 v-model绑定, 子组件通过 modelValue接收,然后给el-form上的model绑定
D. 表单内容: 遍历 formItems
a. 通过v-bind="item.otherOptions",绑定组件特有的属性
b. 绑定值的方式,详见下面的2 【子传父的两种模式】
组件代码分享:

<!--搜索栏区域封装 --> <template> <div class="mySearchArea"> <el-collapse v-model="activeName" accordion class="my-collapse" @change="handleChange"> <el-collapse-item name="firstCollaspe"> <template #title> <div class="my-collapse-title" @click.stop=""> <el-button size="small" type="primary" @click.stop="searchClick">搜索</el-button> <el-button size="small" @click.stop="resetClick">重置</el-button> <!-- 具名插槽,用于外界自己扩展按钮,用法 #myOwnArea --> <slot name="myOwnArea"> </slot> </div> </template> <!-- 搜索区域内容 --> <el-card shadow="never" class="my-card-search"> <el-form ref="searchFormRef" :inline="isInLine" :label-position="labelPosition" :label-width="labelWidth" :model="modelValue"> <template v-for="item in formItems" :key="item.field"> <el-form-item :label="item.label"> <!-- 1. input标签 --> <!-- 特别注意:$event就是修改后的值 --> <template v-if="item.type === 'input'"> <el-input v-bind="item.otherOptions" :placeholder="item.placeholder" :modelValue="myFormData[`${item.field}`]" @update:modelValue="handleValueChange($event, item.field)" @keyup.enter="searchClick" ></el-input> </template> <!-- 2. select标签 --> <template v-else-if="item.type === 'select'"> <el-select v-bind="item.otherOptions" :placeholder="item.placeholder" :modelValue="myFormData[`${item.field}`]" @update:modelValue="handleValueChange($event, item.field)" > <el-option v-for="oItem in item.options" :label="oItem.label" :value="oItem.value"></el-option> </el-select> </template> <!-- 3. date-picker标签 --> <!--特别注意: start-placeholder 和 startPlaceholder 写法是等价的 --> <template v-else-if="item.type === 'datepicker'"> <el-date-picker v-bind="item.otherOptions" :modelValue="myFormData[`${item.field}`]" @update:modelValue="handleValueChange($event, item.field)" > </el-date-picker> </template> </el-form-item> </template> <!-- <el-form-item label="账号"> <el-input placeholder="请输入" v-model.trim="formData.userAccount" @keyup.enter="searchClick"></el-input> </el-form-item> <el-form-item label="姓名"> <el-input placeholder="请输入" v-model.trim="formData.userRealName" @keyup.enter="searchClick"></el-input> </el-form-item> <el-form-item label="性别"> <el-select placeholder="请选择" v-model="formData.userSex" style="width: 181px"> <el-option label="全部" value="-1"></el-option> <el-option label="男" value="0"></el-option> <el-option label="女" value="1"></el-option> </el-select> </el-form-item> <el-form-item label="联系方式"> <el-input placeholder="请输入" v-model.trim="formData.userPhone" @keyup.enter="searchClick"></el-input> </el-form-item> <el-form-item label="创建时间"> <el-date-picker start-placeholder="开始日期" range-separator="至" end-placeholder="结束日期" v-model="formData.operateDateRange" type="daterange" value-format="YYYY-MM-DD" style="width: 479px" > </el-date-picker> </el-form-item> --> </el-form> </el-card> </el-collapse-item> </el-collapse> </div> </template> <script setup> /** *监听搜索框打开or折叠 */ const handleChange = val => { emits('update:handleChange', val); }; // 1. 接收父组件传递过来的值 const props = defineProps({ // 搜索框展开默认显示的区域名称 activeName: { type: String, default: 'firstCollaspe', }, //form表单中的内容, 父组件v-model默认绑定的接收值就是modelValue modelValue: { type: Object, required: true, }, // 表单中form-item中对应标题的宽度 labelWidth: { type: String, default: '85px', }, // 表单域标签的位置 labelPosition: { type: String, default: 'right', }, // 行内表单模式 isInLine: { type: Boolean, default: true, }, // form中各个表单子元素的属性集合 formItems: { type: Array, default: () => [], }, }); // 需要符合单项数据流原则,不要直接对prop中接收的对象进行修改 const myFormData = ref({ ...props.modelValue }); // 2. 声明对外传递事件 // 注意:这里声明的'update:modelValue'和上面input标签中用的不是一个!!! // 上面input中的等价于@input/change, 而这里的'update:modelValue'用于向父组件传递值的 const emits = defineEmits(['update:searchClick', 'update:resetClick', 'update:modelValue', 'update:handleChange']); /** * 搜索事件 */ const searchClick = () => { emits('update:searchClick'); }; /** * 重置事件 */ const resetClick = () => { emits('update:resetClick'); }; /** * 监听form表单中内容的变化 * @param {String、Array} pageInfo 变化后的属性内容 * @param {String} field 对应的属性名称 */ const handleValueChange = (newValue, field) => { // 下面代码针对父组件中的ref对象有效(直接传递...props.modelValue生效,就不符合单项数据流原则了)【不推荐使用了】 // emits('update:modelValue', { ...myFormData.value, [field]: newValue }); // 下面代码针对父组件中的 reactive或ref 对象有效 emits('update:modelValue', Object.assign(myFormData.value, { [field]: newValue })); }; </script> <style scoped lang="scss"> .mySearchArea { margin-bottom: 10px; .el-form--inline .el-form-item { vertical-align: baseline; } } /* 【el-collapse】样式重写 */ .my-collapse { border: none; .my-collapse-title { padding: 0px 5px; } } /* 重写折叠栏的高度 */ .el-collapse { --el-collapse-header-height: 40px; } /* 内部card中body样式重写 */ .my-card-search { border: none; max-height: 122px; box-sizing: border-box; overflow: auto; /* 内容区域的padding */ .el-card__body { padding: 10px 5px; } } </style> <style lang="scss"> /* 样式重写(加上scoped标签无效) */ .el-collapse-item__content { padding-bottom: 0 !important; } </style>
父页面代码分享:

<template> <div> <!-- 1. 搜索栏区域 --> <ypfSearch :activeName="activeName" v-bind="searchFormConfig" v-model="formData" @update:searchClick="searchClick" @update:resetClick="resetClick" @update:handleChange="myHandleChange" > <!-- 此处可以通过具名插槽扩展自己的内容 <template #myOwnArea> </template> --> </ypfSearch> <!-- 2. 内容区域 --> <ypfTable ref="ypfTableRef" v-bind="contentTableConfig" :tableRows="tableData.tableRows" :propList="bindPropList" :tableTotal="tableData.total" :page="tableData.param" @update:page="UpdatePage" @update:tableSelChange="tableSelChange" @update:sortChange="sortChange" > <!-- 2.1 按钮区域 --> <template #btnLeft> <el-button size="small" type="success" @click="onOpenAddDialog" v-auth="authList.add"> 新增 </el-button> <el-button size="small" type="danger" @click="deleteObjs(null)" v-auth="authList.delMany"> 删除 </el-button> </template> <template #btnRight> <el-button size="small" type="primary" round v-auth="authList.excel" @click="exportExcel"> 导出 </el-button> <el-button size="small" type="success" round v-auth="authList.arrange" @click="openSettingDialog"> 设置 </el-button> </template> <!-- 2.2 表格区域 --> <template #userSex="myInfo"> <el-tag type="success" v-if="myInfo.row1.userSex == 0">男</el-tag> <el-tag type="info" v-else>女</el-tag> </template> <template #addTime="myInfo"> {{ formatDate(new Date(myInfo.row1.addTime), 'YYYY-mm-dd') }} </template> <template #handler="myInfo"> <el-button size="small" type="text" @click="onOpenEditDialog(myInfo.row1)" v-auth="authList.edit">修改</el-button> <el-button size="small" type="text" @click="deleteObjs(myInfo.row1.id)" v-auth="authList.delOne">删除</el-button> </template> <!-- 2.3 分页区域 --> </ypfTable> <!-- 3. 弹框区域 --> <AddUer ref="addDialogRef" @updateTableInfo="initTableData" /> <EditUser ref="editDialogRef" @updateTableInfo="initTableData" /> <Setting ref="setDialogRef" @updateTableColumn="initTableColumn" :propList="propList" :pageName="pageName"></Setting> </div> </template> <script setup name="systemUser"> import { getCurrentInstance, nextTick, onMounted, reactive } from 'vue'; import { useStore } from 'vuex'; import { ElMessage } from 'element-plus'; import AddUer from '/@/views/system/user/component/addUser.vue'; import EditUser from '/@/views/system/user/component/editUser.vue'; import Setting from '/@/components/setting/index.vue'; import { myAxios } from '/@/utils/request'; import { formatDate } from '/@/utils/formatTime'; import { auth } from '/@/utils/authFunction'; import { Session } from '/@/utils/storage'; import ypfTable from '/@/components/ypfTable/index.vue'; import ypfSearch from '/@/components/ypfSearch/index.vue'; import { contentTableConfig } from './config/tableConfig'; import { searchFormConfig } from './config/searchFormConfig'; import { TrimObjStrPros } from '/@/utils/myTools'; const { proxy, type } = getCurrentInstance(); const store = useStore(); const pageName = type.name; //当前页面的name值(systemUser) const columnLocalKey = `${store.state.userInfos.userInfos.userAccount}_${pageName}`; //表格显示列数据对应的缓存key, eg: admin_systemUser // 表格对象 const ypfTableRef = ref(null); /** * 监听搜索栏折叠or展开时触发 */ const myHandleChange = () => { // 必须加个延迟,否则获取的高度是折叠前的高度 setTimeout(() => ypfTableRef.value?.calTableHeight(), 500); }; // 按钮权限对象 const authList = reactive({ search: '/system/user/search', add: '/system/user/add', edit: '/system/user/edit', delOne: '/system/user/delOne', delMany: '/system/user/delMany', excel: '/system/user/excel', arrange: '/system/user/arrange', }); // 绑定组件对象区域 const addDialogRef = ref(null); const editDialogRef = ref(null); const setDialogRef = ref(null); // 表格区域 const tableData = reactive({ tableRows: [], //表格数据源 total: 0, selectRowIds: [], //表格选中行的id数组 param: { pageNum: 1, pageSize: 10, sortName: '', //排序字段名称 sortDirection: '', //排序方式 }, }); //表格显示列和排列顺序(默认的) let propList = ref(contentTableConfig.originPropList); // 绑定用的[...propList.value] let bindPropList = ref([]); //搜索栏区域 const activeName = ref('firstCollaspe'); //对应collapse-item的name值,用于默认展开 // 优化:formData中的属性,不需要再写了,完全可以用searchFormConfig配置中的field属性即可 let formOriginData = {}; for (const item of searchFormConfig.formItems) { formOriginData[item.field] = ''; // 下面是处理默认赋值 if (item.field == 'userSex') { formOriginData[item.field] = -1; } } let formData = ref(formOriginData); /** * 初始化表格数据 */ const initTableData = async () => { if (!auth(authList.search)) { ElMessage.error('您没有查询权限'); return; } //将string类型的对象去空格 let myTrimData = TrimObjStrPros(formData.value); const { status, data } = await myAxios({ url: proxy.$url.GetUserInforByConditionUrl, data: { ...tableData.param, ...myTrimData }, }); if (status == 'ok') { tableData.tableRows = data.tableRows; tableData.total = data.total; } }; /** * 初始化表格显示列和顺序 * @param {Array} newColumnData 子页面传递过来的表格显示列和顺序 * 格式: [ { id: '1', label: '账号', columnName: 'userAccount', isSelect: true }, { id: '2', label: '姓名', columnName: 'userRealName', isSelect: false } ] */ const initTableColumn = async newColumnData => { let myColumnData = null; if (newColumnData) { //一.表示从子页面被动调用 myColumnData = newColumnData; } else { // 二. 表示主页面主动加载 //1. 先从缓存中读取 myColumnData = Session.get(columnLocalKey); //2. 缓存中没有,则请求接口 if (!myColumnData) { const { status, data } = await myAxios({ url: proxy.$url.GetUserMenuColumnsUrl, data: { menuName: pageName }, }); if (status == 'ok' && data) { myColumnData = data; Session.set(columnLocalKey, myColumnData); //存到缓存中 } } } //3. 处理数据 if (myColumnData) { //3.1 表示从缓存 或 接口 或子页面传递 中拿到了数据 let latestColumnData = []; myColumnData.forEach(item => { let newItem = propList.value.find(propItem => propItem.prop === item.columnName && item.isSelect == true); if (newItem) latestColumnData.push(newItem); }); bindPropList.value = []; //必须先清空一下,才会触发数据改变 nextTick(() => { bindPropList.value = [...latestColumnData]; }); } else { //3.2 表示缓存或者接口中都没有数据,直接用前端都默认值赋值 nextTick(() => { bindPropList.value = [...propList.value]; }); } }; /** * 搜索事件 */ const searchClick = async () => { // console.log(formData.value); await initTableData(); }; /** * 重置事件 */ const resetClick = async () => { // 清空数据 Object.keys(formData.value).forEach(key => { formData.value[key] = ''; // 下面是处理默认赋值问题 if (key == 'userSex') { formData.value[key] = -1; } }); await initTableData(); }; // 打开新增弹窗 const onOpenAddDialog = () => { addDialogRef.value.openDialog(); }; /** * 打开修改弹窗 * @param {Object} row:该行数据 */ const onOpenEditDialog = row => { editDialogRef.value.openDialog(row); }; /** * 打开设置弹框 */ const openSettingDialog = () => { setDialogRef.value.openDialog(); }; /** * 删除(单个 和 多个) * @param {String} row 单个删除时,删除的id * 对于多个删除,deleteId 为 null */ const deleteObjs = async deleteId => { let delIdsStr = deleteId ?? tableData.selectRowIds; if (delIdsStr.length === 0) { proxy.$message('请选择要删除的行'); return; } let confirmResult = await proxy.$myConfirm('您确定要删除该账户吗?'); if (confirmResult == 'confirm') { const { status, msg } = await myAxios({ url: proxy.$url.DelUserUrl, data: { delIdsStr }, }); if (status === 'ok') { ElMessage.success(msg); await initTableData(); } } }; /** * 监听表格选中行变化 * @param {Array} selections 变化后的数组(数组元素为每行的数据) */ const tableSelChange = selections => { let selectRowIds = selections.map(item => item.id); //返回一个id集合数组 tableData.selectRowIds = selectRowIds; }; /** * 监听子页面传递过来的表格排序参数 * @param {Object} pageInfo {sortName: xx, sortDirection: xx} * 解释:sortName 表格排序列名称, sortDirection 表格排序方式 */ const sortChange = async ({ sortName, sortDirection }) => { if (sortName && sortDirection) { tableData.param.sortName = sortName; tableData.param.sortDirection = sortDirection; await initTableData(); } }; /** * 监听子页面传递的表格分页参数 * @param {Object} pageInfo {pageNum: xx, pageSize: xx} */ const UpdatePage = pageInfo => { if (pageInfo) { //tableData.param 中的sortDirection和sortName被置为 "", 不影响业务 tableData.param = { ...pageInfo }; initTableData(); } }; /** * 按顺序导出显示列的excel表格 */ const exportExcel = async () => { let confirmResult = await proxy.$myConfirm('您确定要导出Excel吗?'); let myTrimData = TrimObjStrPros(formData.value); //将string类型的对象去空格 let searchStr = JSON.stringify(myTrimData); //搜索条件字符串 let pageStr = JSON.stringify(tableData.param); //分页条件字符串 //需要导出列的信息 注:只有含有excelWidth属性的列才是需要导出的列 let bindPropListStr = JSON.stringify(bindPropList.value.filter(item => item.excelWidth)); if (confirmResult == 'confirm') { // 版本1-路径的形式 // window.location.href = proxy.$url.UserDownLoadFileExcelUrl + '?auth=' + Session.get('token') + '&searchStr=' + searchStr + '&pageStr=' + pageStr + '&bindPropListStr=' + bindPropListStr; // 版本2-文件流版本 await myAxios({ url: proxy.$url.UserDownLoadFileExcelUrl2, responseType: 'blob', data: { searchStr, pageStr, bindPropListStr }, }); } }; // 页面实例挂载完成后 onMounted(async () => { // 1. 初始化表格显示列和排列顺序 await initTableColumn(); // 2. 初始化表格数据 await initTableData(); }); </script>
配置文件代码分享:

const searchFormConfig = {
labelWidth: '85px', //表单中form-item中对应标题的宽度
labelPosition: 'right', //表单域标签的位置
isInLine: true, //行内表单模式
formItems: [
{
field: 'userAccount',
type: 'input',
label: '账号',
placeholder: '请输入',
otherOptions: {}, //对应form-item特有属性,比如后面的时间框
},
{
field: 'userRealName',
type: 'input',
label: '姓名',
placeholder: '请输入',
otherOptions: {},
},
{
field: 'userSex',
type: 'select',
label: '性别',
placeholder: '请选择',
options: [
{ label: '全部', value: -1 },
{ label: '男', value: 0 },
{ label: '女', value: 1 },
],
otherOptions: {
style: 'width: 181px',
},
},
{
field: 'userPhone',
type: 'input',
label: '联系方式',
placeholder: '请输入',
otherOptions: {},
},
{
field: 'operateDateRange',
type: 'datepicker',
label: '创建时间',
otherOptions: {
startPlaceholder: '开始时间',
rangeSeparator: '至',
endPlaceholder: '结束时间',
type: 'daterange',
valueFormat: 'YYYY-MM-DD',
style: 'width: 479px',
},
},
],
};
export { searchFormConfig };
2. ypf-search组件传值方案【子传父】
(1). 方案1
子组件通过 :modelValue="myFormData[`event, item.field)"监听变化, 因为要遵循单向数据流原则,绑定的是myFromData, const myFormData = ref({ ...props.modelValue }); 在handleValueChange方法中通过 ,由于中转了一下,绑定的是myFormData,所以通过Object.assign处理后,传值 emits('update:modelValue', Object.assign(props.modelValue, { [field]: newValue })); 向父组件传值, 父组件通过v-model的形式绑定,能监听formData变化的同时,直接赋值,只需要监听 @update:searchClick=“xxx”,里面调用初始化数据方法initTableData即可(该方法内部的formData已经更新赋值)
详见【components/ypfSearch/index.vue 】

<!--搜索栏区域封装 --> <template> <div class="mySearchArea"> <el-collapse v-model="activeName" accordion class="my-collapse" @change="handleChange"> <el-collapse-item name="firstCollaspe"> <template #title> <div class="my-collapse-title" @click.stop=""> <el-button size="small" type="primary" @click.stop="searchClick">搜索</el-button> <el-button size="small" @click.stop="resetClick">重置</el-button> <!-- 具名插槽,用于外界自己扩展按钮,用法 #myOwnArea --> <slot name="myOwnArea"> </slot> </div> </template> <!-- 搜索区域内容 --> <el-card shadow="never" class="my-card-search"> <el-form ref="searchFormRef" :inline="isInLine" :label-position="labelPosition" :label-width="labelWidth" :model="modelValue"> <template v-for="item in formItems" :key="item.field"> <el-form-item :label="item.label"> <!-- 1. input标签 --> <!-- 特别注意:$event就是修改后的值 --> <template v-if="item.type === 'input'"> <el-input v-bind="item.otherOptions" :placeholder="item.placeholder" :modelValue="myFormData[`${item.field}`]" @update:modelValue="handleValueChange($event, item.field)" @keyup.enter="searchClick" ></el-input> </template> <!-- 2. select标签 --> <template v-else-if="item.type === 'select'"> <el-select v-bind="item.otherOptions" :placeholder="item.placeholder" :modelValue="myFormData[`${item.field}`]" @update:modelValue="handleValueChange($event, item.field)" > <el-option v-for="oItem in item.options" :label="oItem.label" :value="oItem.value"></el-option> </el-select> </template> <!-- 3. date-picker标签 --> <!--特别注意: start-placeholder 和 startPlaceholder 写法是等价的 --> <template v-else-if="item.type === 'datepicker'"> <el-date-picker v-bind="item.otherOptions" :modelValue="myFormData[`${item.field}`]" @update:modelValue="handleValueChange($event, item.field)" > </el-date-picker> </template> </el-form-item> </template> <!-- <el-form-item label="账号"> <el-input placeholder="请输入" v-model.trim="formData.userAccount" @keyup.enter="searchClick"></el-input> </el-form-item> <el-form-item label="姓名"> <el-input placeholder="请输入" v-model.trim="formData.userRealName" @keyup.enter="searchClick"></el-input> </el-form-item> <el-form-item label="性别"> <el-select placeholder="请选择" v-model="formData.userSex" style="width: 181px"> <el-option label="全部" value="-1"></el-option> <el-option label="男" value="0"></el-option> <el-option label="女" value="1"></el-option> </el-select> </el-form-item> <el-form-item label="联系方式"> <el-input placeholder="请输入" v-model.trim="formData.userPhone" @keyup.enter="searchClick"></el-input> </el-form-item> <el-form-item label="创建时间"> <el-date-picker start-placeholder="开始日期" range-separator="至" end-placeholder="结束日期" v-model="formData.operateDateRange" type="daterange" value-format="YYYY-MM-DD" style="width: 479px" > </el-date-picker> </el-form-item> --> </el-form> </el-card> </el-collapse-item> </el-collapse> </div> </template> <script setup> /** *监听搜索框打开or折叠 */ const handleChange = val => { emits('update:handleChange', val); }; // 1. 接收父组件传递过来的值 const props = defineProps({ // 搜索框展开默认显示的区域名称 activeName: { type: String, default: 'firstCollaspe', }, //form表单中的内容, 父组件v-model默认绑定的接收值就是modelValue modelValue: { type: Object, required: true, }, // 表单中form-item中对应标题的宽度 labelWidth: { type: String, default: '85px', }, // 表单域标签的位置 labelPosition: { type: String, default: 'right', }, // 行内表单模式 isInLine: { type: Boolean, default: true, }, // form中各个表单子元素的属性集合 formItems: { type: Array, default: () => [], }, }); // 需要符合单项数据流原则,不要直接对prop中接收的对象进行修改 const myFormData = ref({ ...props.modelValue }); // 2. 声明对外传递事件 // 注意:这里声明的'update:modelValue'和上面input标签中用的不是一个!!! // 上面input中的等价于@input/change, 而这里的'update:modelValue'用于向父组件传递值的 const emits = defineEmits(['update:searchClick', 'update:resetClick', 'update:modelValue', 'update:handleChange']); /** * 搜索事件 */ const searchClick = () => { emits('update:searchClick'); }; /** * 重置事件 */ const resetClick = () => { emits('update:resetClick'); }; /** * 监听form表单中内容的变化 * @param {String、Array} pageInfo 变化后的属性内容 * @param {String} field 对应的属性名称 */ const handleValueChange = (newValue, field) => { // 下面代码针对父组件中的ref对象有效(直接传递...props.modelValue生效,就不符合单项数据流原则了)【不推荐使用了】 // emits('update:modelValue', { ...myFormData.value, [field]: newValue }); // 下面代码针对父组件中的 reactive或ref 对象有效 emits('update:modelValue', Object.assign(myFormData.value, { [field]: newValue })); }; </script> <style scoped lang="scss"> .mySearchArea { margin-bottom: 10px; .el-form--inline .el-form-item { vertical-align: baseline; } } /* 【el-collapse】样式重写 */ .my-collapse { border: none; .my-collapse-title { padding: 0px 5px; } } /* 重写折叠栏的高度 */ .el-collapse { --el-collapse-header-height: 40px; } /* 内部card中body样式重写 */ .my-card-search { border: none; max-height: 122px; box-sizing: border-box; overflow: auto; /* 内容区域的padding */ .el-card__body { padding: 10px 5px; } } </style> <style lang="scss"> /* 样式重写(加上scoped标签无效) */ .el-collapse-item__content { padding-bottom: 0 !important; } </style>
(2). 方案2
子组件通过v-model绑定数据,赋值的同时进行监听,注意,这里并没有直接绑定modelValue, 因为要遵循单向数据流原则,绑定的是myFromData, const myFormData = ref({ ...props.modelValue }); 然后通过watch监听myFormData的变化,并通过emits('update:modelValue', newValue);对外暴露, 父组件通过v-model的形式绑定,能监听formData变化的同时,直接赋值,只需要监听 @update:searchClick=“xxx”,里面调用初始化数据方法initTableData即可(该方法内部的formData已经更新赋值)
详见【components/ypfSearch/index_v2.vue 】

<!--搜索栏区域封装 --> <template> <div class="mySearchArea"> <el-collapse v-model="activeName" accordion class="my-collapse" @change="handleChange"> <el-collapse-item name="firstCollaspe"> <template #title> <div class="my-collapse-title" @click.stop=""> <el-button size="small" type="primary" @click.stop="searchClick">搜索</el-button> <el-button size="small" @click.stop="resetClick">重置</el-button> <!-- 具名插槽,用于外界自己扩展按钮,用法 #myOwnArea --> <slot name="myOwnArea"> </slot> </div> </template> <!-- 搜索区域内容 --> <el-card shadow="never" class="my-card-search"> <el-form ref="searchFormRef" :inline="isInLine" :label-position="labelPosition" :label-width="labelWidth" :model="modelValue"> <template v-for="item in formItems" :key="item.field"> <el-form-item :label="item.label"> <!-- 1. input标签 --> <!-- 特别注意:$event就是修改后的值 --> <template v-if="item.type === 'input'"> <el-input v-bind="item.otherOptions" :placeholder="item.placeholder" v-model="myFormData[`${item.field}`]" @keyup.enter="searchClick" ></el-input> </template> <!-- 2. select标签 --> <template v-else-if="item.type === 'select'"> <el-select v-bind="item.otherOptions" :placeholder="item.placeholder" v-model="myFormData[`${item.field}`]"> <el-option v-for="oItem in item.options" :label="oItem.label" :value="oItem.value"></el-option> </el-select> </template> <!-- 3. date-picker标签 --> <!--特别注意: start-placeholder 和 startPlaceholder 写法是等价的 --> <template v-else-if="item.type === 'datepicker'"> <el-date-picker v-bind="item.otherOptions" v-model="myFormData[`${item.field}`]"> </el-date-picker> </template> </el-form-item> </template> </el-form> </el-card> </el-collapse-item> </el-collapse> </div> </template> <script setup> import { watch } from '@vue/runtime-core'; /** *监听搜索框打开or折叠 */ const handleChange = val => { emits('update:handleChange', val); }; // 1. 接收父组件传递过来的值 const props = defineProps({ // 搜索框展开默认显示的区域名称 activeName: { type: String, default: 'firstCollaspe', }, //form表单中的内容, 父组件v-model默认绑定的接收值就是modelValue modelValue: { type: Object, required: true, }, // 表单中form-item中对应标题的宽度 labelWidth: { type: String, default: '85px', }, // 表单域标签的位置 labelPosition: { type: String, default: 'right', }, // 行内表单模式 isInLine: { type: Boolean, default: true, }, // form中各个表单子元素的属性集合 formItems: { type: Array, default: () => [], }, }); // 需要符合单项数据流原则,不要直接对prop中接收的对象进行修改 const myFormData = ref({ ...props.modelValue }); // 2. 声明对外传递事件 const emits = defineEmits(['update:searchClick', 'update:resetClick', 'update:modelValue']); /** * 搜索事件 */ const searchClick = () => { emits('update:searchClick'); }; /** * 重置事件 */ const resetClick = () => { emits('update:resetClick'); }; /** * 监听form表单中内容的变化 */ watch( myFormData, newValue => { emits('update:modelValue', newValue); }, { deep: true } ); </script> <style scoped lang="scss"> .mySearchArea { margin-bottom: 10px; .el-form--inline .el-form-item { vertical-align: baseline; } } /* 【el-collapse】样式重写 */ .my-collapse { border: none; .my-collapse-title { padding: 0px 5px; } } /* 重写折叠栏的高度 */ .el-collapse { --el-collapse-header-height: 40px; } /* 内部card中body样式重写 */ .my-card-search { border: none; max-height: 122px; box-sizing: border-box; overflow: auto; /* 内容区域的padding */ .el-card__body { padding: 10px 5px; } } </style> <style lang="scss"> /* 样式重写(加上scoped标签无效) */ .el-collapse-item__content { padding-bottom: 0 !important; } </style>
总结:方案2写法更加简洁,无论用哪种方案,父页面中的业务代码都不需要改
3. 其它技术点剖析
(1). 父组件重置对象的方案
A. 针对ypf-search方案1的写法,父组件可以直接 formData.value = {}; 清空数据即可
B. 针对ypf-search方案2的写法,父组件需要通过遍历属性的写法清空数据 (这种写法是通用的,也适合方案1)
let formOriginData = {};
for (const item of searchFormConfig.formItems) {
formOriginData[item.field] = '';
// 下面是处理默认赋值
if (item.field == 'userSex') {
formOriginData[item.field] = -1;
}
}
(2). 搜索框默认赋值的问题
A. 首先父组件中组装formData的时候需要判断,赋默认值
B. 父组件中重置方法resetClick中需要赋默认值
(3). 搜索条件去空格方案 【考虑抽离封装一下!!】
主要针对表格加载方法“initTableData” 和 导出Excel方法“exportExcel”中的 fromData.value,进行处理: 先深拷贝给一个新对象(否则响应式相互影响) → 遍历获取string类型属性→ 去空格
4. 其它记录
(1). :inline='true',可以让多个表单元素显示在一行上,如果设置为false,则一个<el-form-item/>一行
(2). 在<el-form/>上设置 label-width="85px",表示下面所有的 <el-form-item/>标题的宽度为85px,注意是标题(label),不是输入框。 也可以直接加在<el-form-item/>上,优先级高于加在<el-form/>上的
(3). 普通的input输入框宽度为181,所以把select下拉框width设置成181,时间选择框width=181+181+85(label)+32(右边距)=479px
(4). 对于不同的输入框,比如select、date-picker,需要在style中设置样式,比如 style="width: 181px"
二. 弹框的使用
1. 如果单独封装一个弹框页面, 那么意味着就没有addUser.vue、editUser.vue这些页面了,所有的业务逻辑都集中index主页面中和封装弹框页面里了;
另外新增和编辑的api地址不同,通过传值的方式也不是很友好,所以这里目前不倾向彻底封装。
2. 考虑将dialog中的<el-form/>抽离出来,这样既可以简化每个弹框页面的代码,又可以保留这些页面,而且index主页面代码不需要改,目前来看是比较好的方案.
3. dialog内部引用了el-row 和 el-cow响应式布局,不利于封装
综上所述,弹框组件【暂时不封装】,后面有了好的思路再决定
三. 其它
后面补充
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
2019-01-15 第二节:如何正确使用WebApi和使用过程中的一些坑
2018-01-15 第十一节:深究用户模式锁的使用场景(异变结构、互锁、旋转锁)