第五节:Form组件封装和基于Form组件的PageSearch、PageModal组件的封装


一. 整体说明

1. 整体规划

 首先利用el-form组件,封装一个 ypf-form组件,可以实现通过传入配置,显示各种Form表单。

 然后封装 page-search 搜索框组件,该组件基于 YpfForm

 然后封装 page-Modal 弹框组件,该组件基于 YpfForm

各组件的调用次序如下: 

 ypf-form:表单组件 → page-search:搜索框组件 → user.vue:用户管理页面 → search.config.ts 搜索框的配置文件

 ypf-form: 表单组件 → page-modal:弹框组件 → user.vue:用户管理页面 → modal.config.ts 弹框的配置文件

2. 组件介绍 

(1). ypf-form

 form表单组件,主要支持 'input' | 'password' | 'select' | 'datepicker' 四种表单组件。

(2). page-search

 页面搜索框组件, 处理表格的搜索业务

(3). page-modal

 页面弹框组件 ,主要用来处理新增 和 编辑 等弹框逻辑

二. Form组件封装

1. 封装思路

(1). 首先该组件分为三部分,顶部是name为header的插槽,底部是name为footer的插槽,中间部分是利用el-form组成各种表单元素。(一般顶部header插槽调用者用来存放标题之类,footer插槽调用者用来存放按钮之类)

(2). 该组件接收的样式和布局方面的参数有:

 A. labelWidth : 表单域的宽度,绑定在最外层el-form上

 B. colLayout:表单的响应式布局

 C. itemStyle:表单子元素的样式,绑定在el-form-item上的style属性里

(3). 该组件接收的表单类别和表单属性的参数为:formItems详细参数如下,  具体分析:fieId、type、label、placeholder为表单通用元素,rules为表单验证规则,一些特有的属性通过otherOptions配置,通过isHidden配合v-if控制是否显示。

type IFormType = 'input' | 'password' | 'select' | 'datepicker';
export interface IFormItem {
    // 标识id
    field: string;
    // 表单类型,支持上述四种类型
    type: IFormType;
    // 表单名称
    label: string;
    // 输入框占位文本
    placeholder?: any;
    // 表单校验规则
    rules?: any[];
    // 针对select选择框使用
    options?: any[];
    // 针对不同标签特有属性
    otherOptions?: any;
    // 动态控制form中某个元素的显示和隐藏
    isHidden?: boolean;
}

传入的配置文件serach.config.ts代码如下:

IForm接口

export interface IForm {
    formItems: IFormItem[];
    labelWidth?: string;
    colLayout: any;
    itemLayout: any;
}

配置代码

import { IForm } from '@/base-ui/form';

export const searchFormConfig: IForm = {
    labelWidth: '120px',
    itemLayout: {
        padding: '5px 5px',
    },
    colLayout: {
        span: 8,
    },
    formItems: [
        {
            field: 'id',
            type: 'input',
            label: 'id',
            placeholder: '请输入id',
            otherOptions: {
                size: 'small',
            },
        },
        {
            field: 'name',
            type: 'input',
            label: '用户名',
            placeholder: '请输入用户名',
        },
        {
            field: 'realname',
            type: 'input',
            label: '真实姓名',
            placeholder: '请输入真实姓名',
        },
        {
            field: 'cellphone',
            type: 'input',
            label: '电话号码',
            placeholder: '请输入电话号码',
        },
        {
            field: 'enable',
            type: 'select',
            label: '用户状态',
            placeholder: '请选择用户状态',
            options: [
                { title: '启用', value: 1 },
                { title: '禁用', value: 0 },
            ],
        },
        {
            field: 'createAt',
            type: 'datepicker',
            label: '创建时间',
            otherOptions: {
                startPlaceholder: '开始时间',
                endPlaceholder: '结束时间',
                type: 'daterange',
            },
        },
    ],
};

(4). 该组件接收到底表单元素的内容值得参数为:modelValue

  父组件调用该组件的时候,如果通过v-model绑定一个值,那么子组件默认就是通过modelValue来接收,这里组件通过v-model来绑定值,然后通过watch 监听,通过  emit('update:modelValue', newValue);对外暴露新值。

封装代码分享:

<template>
    <div class="ypf-form">
        <div class="header">
            <slot name="header">slot>
        div>
        <el-form :label-width="labelWidth">
            <el-row>
                <template v-for="item in formItems" :key="item.label">
                    <el-col v-bind="colLayout">
                        <el-form-item v-if="!item.isHidden" :label="item.label" :rules="item.rules" :style="itemStyle">
                            <template v-if="item.type === 'input' || item.type === 'password'">
                                <el-input
                                    v-bind="item.otherOptions"
                                    v-model="myFormData[`${item.field}`]"
                                    :placeholder="item.placeholder"
                                    :show-password="item.type === 'password'"
                                />
                            template>
                            <template v-else-if="item.type === 'select'">
                                <el-select
                                    v-bind="item.otherOptions"
                                    v-model="myFormData[`${item.field}`]"
                                    :placeholder="item.placeholder"
                                    style="width: 100%"
                                >
                                    <el-option v-for="option in item.options" :key="option.value" :value="option.value">
                                        {{ option.title }}
                                    el-option>
                                el-select>
                            template>
                            <template v-else-if="item.type === 'datepicker'">
                                <el-date-picker
                                    style="width: 100%"
                                    v-bind="item.otherOptions"
                                    v-model="myFormData[`${item.field}`]"
                                >el-date-picker>
                            template>
                        el-form-item>
                    el-col>
                template>
            el-row>
        el-form>
        <div class="footer">
            <slot name="footer">slot>
        div>
    div>
template>

<script lang="ts">
    import { defineComponent, PropType, ref, watch } from 'vue';
    import { IFormItem } from '../types';

    export default defineComponent({
        props: {
            // v-model默认接收值就是modelValue
            modelValue: {
                type: Object,
                required: true,
            },
            // form各种表单元素
            formItems: {
                type: Array as PropType<IFormItem[]>,
                default: () => [],
            },
            // 表单域标签的宽度
            labelWidth: {
                type: String,
                default: '100px',
            },
            // 表单子元素的样式
            itemStyle: {
                type: Object,
                default: () => ({ padding: '5px 5px' }),
            },
            // el-row 和 el-cow响应式布局
            colLayout: {
                type: Object,
                default: () => ({
                    xl: 6, // >1920px    lg: 8,
                    md: 12,
                    sm: 24,
                    xs: 24,
                }),
            },
        },
        emits: ['update:modelValue'],
        setup(props, { emit }) {
            // 1. 获取传递的数据{ ...props.modelValue } 是object对象
            const myFormData = ref({ ...props.modelValue });

            // 2. 监听变化,对外传递
            watch(
                myFormData,
                (newValue) => {
                    emit('update:modelValue', newValue);
                },
                {
                    deep: true,
                },
            );

            return { myFormData };
        },
    });
script>

<style scoped lang="less">
    .ypf-form {
        padding-top: 5px;
    }
style>

2. 重难点剖析

(1). v-model绑定

 (详细用法可参考:https://www.cnblogs.com/yaopengfei/p/15347532.html)  【先仔细看!!!】

对于子组件的封装而言:

  v-model="myFormData[`${item.field}`]" ,是一个语法糖,相当于两步操作:① 绑定元素value(element plus中叫modelvalue)的同时,② 监听其value的变化。

等价于:

 A. :modelValue="modelValue[`${item.field}`]"    @update:modelValue="handleValueChange($event, item.field)"             [PS. 这里的$event就是变化后最新值]     

 B. :modelValue="modelValue[`${item.field}`]"   @input="handleValueChange($event, item.field)"  (select标签是:   @change="handleValueChange($event, item.field)")

注意:如果父组件用v-model=“xxx”绑定一个值,子组件需要用 modelValue来接收,这是一个内置默认值。

组件封装写法2:

<template>
    <div class="hy-form">
        <div class="header">
            <slot name="header">slot>
        div>
        <el-form :label-width="labelWidth">
            <el-row>
                <template v-for="item in formItems" :key="item.label">
                    <el-col v-bind="colLayout">
                        <el-form-item :label="item.label" :rules="item.rules" :style="itemStyle">
                            <template v-if="item.type === 'input' || item.type === 'password'">
                                
                                <el-input
                                    v-bind="item.otherOptions"
                                    :placeholder="item.placeholder"
                                    :show-password="item.type === 'password'"
                                    :modelValue="modelValue[`${item.field}`]"
                                    @update:modelValue="handleValueChange($event, item.field)"
                                />
                            template>
                            <template v-else-if="item.type === 'select'">
                                <el-select
                                    v-bind="item.otherOptions"
                                    :placeholder="item.placeholder"
                                    :modelValue="modelValue[`${item.field}`]"
                                    @update:modelValue="handleValueChange($event, item.field)"
                                    style="width: 100%"
                                >
                                    <el-option v-for="option in item.options" :key="option.value" :value="option.value">
                                        {{ option.title }}
                                    el-option>
                                el-select>
                            template>
                            <template v-else-if="item.type === 'datepicker'">
                                <el-date-picker
                                    style="width: 100%"
                                    v-bind="item.otherOptions"
                                    :modelValue="modelValue[`${item.field}`]"
                                    @update:modelValue="handleValueChange($event, item.field)"
                                >el-date-picker>
                            template>
                        el-form-item>
                    el-col>
                template>
            el-row>
        el-form>
        <div class="footer">
            <slot name="footer">slot>
        div>
    div>
template>

<script lang="ts">
    import { defineComponent, PropType, ref, watch } from 'vue';
    import { IFormItem } from '../types';

    export default defineComponent({
        props: {
            // v-model默认接收值就是modelValue
            modelValue: {
                type: Object,
                required: true,
            },
            // form各种表单元素
            formItems: {
                type: Array as PropType<IFormItem[]>,
                default: () => [],
            },
            // 表单域标签的宽度
            labelWidth: {
                type: String,
                default: '100px',
            },
            // 表单子元素的样式
            itemStyle: {
                type: Object,
                default: () => ({ padding: '5px 5px' }),
            },
            // el-row 和 el-cow响应式布局
            colLayout: {
                type: Object,
                default: () => ({
                    xl: 6, // >1920px    lg: 8,
                    md: 12,
                    sm: 24,
                    xs: 24,
                }),
            },
        },
        emits: ['update:modelValue'],
        setup(props, { emit }) {
            const handleValueChange = (newValue: any, field: string) => {
                // 后面相当于 [field]属性在 prop.modelvalue中已经包含了,这里相当于合并了
                emit('update:modelValue', { ...props.modelValue, [field]: newValue });
            };

            return { handleValueChange };
        },
    });
script>

<style scoped lang="less">
    .hy-form {
        padding-top: 5px;
    }
style>

组件封装写法3:

<template>
    <div class="hy-form">
        <div class="header">
            <slot name="header">slot>
        div>
        <el-form :label-width="labelWidth">
            <el-row>
                <template v-for="item in formItems" :key="item.label">
                    <el-col v-bind="colLayout">
                        <el-form-item :label="item.label" :rules="item.rules" :style="itemStyle">
                            <template v-if="item.type === 'input' || item.type === 'password'">
                                
                                <el-input
                                    v-bind="item.otherOptions"
                                    :placeholder="item.placeholder"
                                    :show-password="item.type === 'password'"
                                    :modelValue="modelValue[`${item.field}`]"
                                    @input="handleValueChange($event, item.field)"
                                />
                            template>
                            <template v-else-if="item.type === 'select'">
                                <el-select
                                    v-bind="item.otherOptions"
                                    :placeholder="item.placeholder"
                                    :modelValue="modelValue[`${item.field}`]"
                                    @change="handleValueChange($event, item.field)"
                                    style="width: 100%"
                                >
                                    <el-option v-for="option in item.options" :key="option.value" :value="option.value">
                                        {{ option.title }}
                                    el-option>
                                el-select>
                            template>
                            <template v-else-if="item.type === 'datepicker'">
                                <el-date-picker
                                    style="width: 100%"
                                    v-bind="item.otherOptions"
                                    :modelValue="modelValue[`${item.field}`]"
                                    @change="handleValueChange($event, item.field)"
                                >el-date-picker>
                            template>
                        el-form-item>
                    el-col>
                template>
            el-row>
        el-form>
        <div class="footer">
            <slot name="footer">slot>
        div>
    div>
template>

<script lang="ts">
    import { defineComponent, PropType, ref, watch } from 'vue';
    import { IFormItem } from '../types';

    export default defineComponent({
        props: {
            // v-model默认接收值就是modelValue
            modelValue: {
                type: Object,
                required: true,
            },
            // form各种表单元素
            formItems: {
                type: Array as PropType<IFormItem[]>,
                default: () => [],
            },
            // 表单域标签的宽度
            labelWidth: {
                type: String,
                default: '100px',
            },
            // 表单子元素的样式
            itemStyle: {
                type: Object,
                default: () => ({ padding: '5px 5px' }),
            },
            // el-row 和 el-cow响应式布局
            colLayout: {
                type: Object,
                default: () => ({
                    xl: 6, // >1920px    lg: 8,
                    md: 12,
                    sm: 24,
                    xs: 24,
                }),
            },
        },
        emits: ['update:modelValue'],
        setup(props, { emit }) {
            const handleValueChange = (newValue: any, field: string) => {
                // 后面相当于 [field]属性在 prop.modelvalue中已经包含了,这里相当于合并了
                emit('update:modelValue', { ...props.modelValue, [field]: newValue });
            };

            return { handleValueChange };
        },
    });
script>

<style scoped lang="less">
    .hy-form {
        padding-top: 5px;
    }
style>

(2). v-bind绑定一个对象,可以直接把该对象上的属性绑定到该input元素上

    <el-input  v-bind="item.otherOptions"/>

如果item.otherOptions为

    otherOptions: {
                size: 'small',
                maxlength: "200",
    },

那么最终渲染后的代码为:

  "small" maxlength="200" />

(3). 具名插槽

(详细用法可参考:https://www.cnblogs.com/yaopengfei/p/15338752.html)

下面代码是 名字为header的插槽

<div class="header">
    <slot name="header">slot>
div>

父组件在调用的时候,只需要在