第五节:搜索组件封装剖析、弹框的使用、页面各组件联调

一. 搜索框组件封装

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>
View Code

父页面代码分享:

<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>
View Code

配置文件代码分享:

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 };
View Code

 

2. ypf-search组件传值方案【子传父】

(1). 方案1

   子组件通过 :modelValue="myFormData[`${item.field}`]" 绑定值,通过@update:modelValue="handleValueChange($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>
View Code

 

(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>
View Code

 

总结:方案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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2023-01-15 16:23  Yaopengfei  阅读(297)  评论(1编辑  收藏  举报