第五节: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;
}

传入的配置文件search.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',
            },
        },
    ],
};
View Code

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

2. 重难点剖析

(1). v-model绑定

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

对于el-input这个子组件而言:

  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)")

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'">
                                <!-- 特别注意,下面的$event就是修改后的值 -->
                                <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>
View Code

组件封装写法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'">
                                <!-- 特别注意,下面的$event就是修改后的值 -->
                                <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>
View Code

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

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

如果item.otherOptions为

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

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

  <el-input size="small" maxlength="200" />

(3). 具名插槽

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

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

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

父组件在调用的时候,只需要在 <template>上加个 #header,可以写插槽中的内容了,替换子组件slot的位置 。(在父组件中,调用插槽的时候可以写在任何位置,没有先后)

 

 

三. page-search组件封装

1. 封装思路

(1). 通过v-model将内容对象处理后的内容对象 formData绑定给ypf-form,从而实现双向绑定。

(2). 接收到表单数据为 searchFormConfig,然后再通过v-bind将其内部的属性绑定到 ypf-form 组件上。

(3). 引用ypf-form组件,#header插槽放标题 "搜索",#footer插槽放两个按钮,搜索 和 重置 。

 A. 搜索:先清空formData对象,然后通过 emit('resetBtnClick'); 对外暴露,供父组件调用

 B. 重置:通过 emit('queryBtnClick', formData.value);对外暴露,供父组件调用。

组件代码分享:

<template>
    <div class="page-search">
        <ypf-form v-bind="searchFormConfig" v-model="formData">
            <template #header>
                <h1 class="header">搜索区</h1>
            </template>
            <template #footer>
                <div class="handle-btns">
                    <el-button type="primary" size="small" @click="handleResetClick">重置</el-button>
                    <el-button type="success" size="small" @click="handleQueryClick">搜索</el-button>
                </div>
            </template>
        </ypf-form>
    </div>
</template>

<script lang="ts">
    import { defineComponent, ref } from 'vue';
    import YpfForm from '@/base-ui/form';

    export default defineComponent({
        props: {
            searchFormConfig: {
                type: Object,
                required: true,
            },
        },
        components: {
            YpfForm,
        },
        emits: ['resetBtnClick', 'queryBtnClick'],
        setup(props, { emit }) {
            // 组件绑定的v-model数据
            // let formData = ref({
            //     id: '',
            //     name: '',
            //     password: '',
            //     sport: '',
            //     createTime: '',
            // });

            // 优化1:上述formData中的属性,不需要再写了,完全可以用searchFormConfig配置中的field属性即可
            // 即属性由配置中的field动态决定
            const formItems = props.searchFormConfig?.formItems ?? [];
            const formOriginData: any = {};
            for (const item of formItems) {
                formOriginData[item.field] = '';
            }
            const formData = ref(formOriginData);

            // 2. 重置按钮事件
            const handleResetClick = () => {
                // 2.1 置空
                // 写法1:(特别注意,遍历对象是 for-in)--(对应form组件中写法)
                for (const key in formOriginData) {
                    formData.value[`${key}`] = formOriginData[key];
                    // 等价于 (实际上就是遍历置空)
                    // formData.value[`${key}`] = '';
                }
                // 写法2:(对应form_test1组件中写法)
                // formData.value = formOriginData;

                // 2.2 对外反馈
                emit('resetBtnClick');
            };

            // 3. 搜索按钮事件
            const handleQueryClick = () => {
                emit('queryBtnClick', formData.value);
            };

            return { formData, handleResetClick, handleQueryClick };
        },
    });
</script>

<style scoped lang="less">
    .header {
        font-size: 15px;
        text-align: left;
        padding-left: 10px;
    }
    .handle-btns {
        text-align: right;
        padding: 0 10px 10px 0;
    }
</style>
View Code

2. 重难点剖析

(1). 父组件通过v-model传值,实现双向绑定,其中v-model相当于省略了什么?

 对于父组件而言,通过v-model绑定一个值传给子组件,当在子组件中修改这个绑定值得时候,父组件中绑定到这个值会自动更新!!!   当子组件中的内容改变时,对父组件而言,v-model干了两件事:① 绑定value值  ② 拿到更新后的值,给父组件原值赋值

 如下:三种写法等价(其中handle1中就是把$event赋值给message)

(2). 绑定的FormData属性,由配置文件动态创建?

 我们这里给ypf-form组件通过v-model绑定的 formData 是根据配置中的field动态创建而来的。

配置代码如下:

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',
                maxlength: '200',
            },
        },
        {
            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',
            },
        },
    ],
};
View Code

formData代码组装代码如下:

           // 优化1:上述formData中的属性,不需要再写了,完全可以用searchFormConfig配置中的field属性即可
            // 即属性由配置中的field动态决定
            const formItems = props.searchFormConfig?.formItems ?? [];
            const formOriginData: any = {};
            for (const item of formItems) {
                formOriginData[item.field] = '';
            }
            const formData = ref(formOriginData);

扩展: formData的其它写法

写法1. formData直接写个空对象绑定即可,因为实现了双向绑定,自动就可以绑定属性值。(后面page-modal组件就是这种写法)

// 默认空对象,然后双向绑定,input标签中输入值,formData中就有了这个属性值(新写法!!)
 const formData = ref<any>({});

写法2. 考虑在search.config.js中增加一个属性value,用来存放各个form表单的值,就不需要单独再绑定内容了。【尚未实际测试,需要综合考虑???

(3).重置逻辑的两种写法,各有什么区别?

写法1:

 当ypf-form组件中是通过v-model绑定,通过watch监听的时候,父组件page-search中,需要逐个属性清空来实现重置

ypf-form组件的写法:

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

重置的写法:

const handleResetClick = () => {
                // 2.1 置空
                // 写法1:(特别注意,遍历对象是 for-in)--(对应form组件中写法)
                for (const key in formOriginData) {
                    formData.value[`${key}`] = '';
                }
                // 2.2 对外反馈
                emit('resetBtnClick');
            };

扩展:为什么直接 formData={},不生效呢?

 因为在 ypf-form子组件中,const myFormData = ref({ ...props.modelValue }); 最终v-model绑定的是myFormData(而不是modelvalue值),其中 { ...props.modelValue } ,是做了一个浅拷贝,把浅拷贝后的对象赋值给了myFormData,所以在父组件中,直接修改formData的值,影响到是子组件中的modelValue值,而无法直接影响到{ ...props.modelValue }浅拷贝后的数据。

写法2:

 当ypf-form组件中使用 :modelValue绑定值,通过@update:modelValue监听值的变化的时候,父组件page-search中,重置按钮的逻辑可以使用 formData={},来清空。

ypf-form组件的写法

<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'">
                                <!-- 特别注意,下面的$event就是修改后的值 -->
                                <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>
View Code

重置的写法: 

        // 2. 重置按钮事件
            const handleResetClick = () => {
                // 2.1 置空
                // 写法2:(对应form_test1组件中写法)
                // formData.value = formOriginData;
                // 写法2也可以下面这种写法
                formData.value = {};
                // 2.2 对外反馈
                emit('resetBtnClick');
            };

 

四. page-modal组件封装

1. 封装思路

(1). 该组件由 dialog 和 ypf-form 两个组件构成,dialog的结构为:ypf-form组件、默认插槽、调用dialog的footer插槽,各部分的作用为:

 A. ypf-form组件:用于展示各种form表单

 B. 默认插槽:用于对外提供额外的内容,比如:新增角色的时候,除了表单外,还需要一个tree,用来显示权限,就可以放到这个额外的插槽里。

 C. 调用#footer插槽:用来处理确定 和 取消按钮。

(2). 接收父组件传过来的值有:

 A. modalConfig:用来v-bind绑定给ypf-form组件,显示form表单内容和配置form表单中的样式

 B. pageName:用来后续拼接请求地址的 (如:user、roles 等)

 C. dialogConfig:用来v-bind绑定给el-dialog组件,控制el-dialog样式的

 D. defaultInfo:用来接收新增或编辑打开弹框传递过来的内容,处理编辑显示 和 新增关闭清空问题

 E. otherInfo:主要是用于父组件调用page-modal的默认插槽,插槽中需要传递的内容 (比如:新增角色下面的树)

(3). el-dialog组件配置几个通用的属性:

 A. 通过v-model绑定dialogVisible属性,通过该属性的true 或 false,控制弹框的显示 or 隐藏。

 B. 设置destory-on-close,关闭就销毁属性。header (如果有)footer (如果有)不会被销毁

 C. @closed监听关闭,在里面可以处理清空逻辑。(本次组件封装没有使用)

(4). ypf-form组件,通过v-model绑定一个一个空的formData数据,通过父子组件的双向绑定,可以实现自动填充。

组件代码分享:

<template>
    <div class="page-modal">
        <el-dialog v-model="dialogVisible" v-bind="dialogConfig" destroy-on-close @closed="clearContent">
            <ypf-form v-bind="modalConfig" v-model="formData"></ypf-form>
            <!-- 默认插槽 -->
            <slot></slot>
            <template #footer>
                <span class="dialog-footer">
                    <el-button @click="dialogVisible = false" size="small">取消</el-button>
                    <el-button type="primary" @click="handleConfirmClick" size="small">确定</el-button>
                </span>
            </template>
        </el-dialog>
    </div>
</template>

<script lang="ts">
    import { defineComponent, ref, watch } from 'vue';
    import YpfForm from '@/base-ui/form';
    import { useStore } from '@/store';

    export default defineComponent({
        components: {
            YpfForm,
        },
        props: {
            // 用来存放form表单种类及其表单属性→给ypf-form组件绑定
            modalConfig: {
                type: Object,
                required: true,
            },
            // 主要是用来后续拼接请求地址的
            pageName: {
                type: String,
                required: true,
            },
            // 用来配置el-dialog属性的
            dialogConfig: {
                type: Object,
                default: () => ({
                    title: '新增',
                    width: '600px',
                    center: false,
                }),
            },
            // 用来接收新增 or 编辑传递过来的对象
            defaultInfo: {
                type: Object,
                default: () => ({}),
            },
            // 额外的参数,比如角色新增 下面树选中的节点
            otherInfo: {
                type: Object,
                default: () => ({}),
            },
        },
        setup(props) {
            // dialog数据
            const dialogVisible = ref(false);
            // 默认空对象,然后双向绑定,input标签中输入值,formData中就有了这个属性值(新写法!!)
            const formData = ref<any>({});

            // 重点理解此处watch的作用
            // 如何实现了编辑的显示功能 和 新增输入内容,关闭弹框内容消失的功能
            // 说明:打开新增 或 编辑 弹框的时候,在其方法里都会给defaultInfo赋值,新增是 proxy{},编辑是 含内容的对象,都相当于defaultInfo发生了改变
            // 所以会触发这里watch监听,下面的newValue就是传递过来的新值;对于新增,空对象就相当于清空了;对于编辑有内容,相当于回显了。
            watch(
                () => props.defaultInfo,
                (newValue) => {
                    // console.log('监控defaultInfo', newValue);
                    for (const item of props.modalConfig.formItems) {
                        formData.value[`${item.field}`] = newValue[`${item.field}`];
                    }
                },
            );

            // 确定事件
            const store = useStore();
            const handleConfirmClick = () => {
                dialogVisible.value = false;
                // 判断defaultInfo是否有元素
                if (Object.keys(props.defaultInfo).length) {
                    // 编辑
                    console.log('编辑用户');
                    store.dispatch('system/editPageDataAction', {
                        pageName: props.pageName,
                        editData: { ...formData.value, ...props.otherInfo },
                        id: props.defaultInfo.id,
                    });
                } else {
                    // 新建
                    console.log('新建用户');
                    store.dispatch('system/createPageDataAction', {
                        pageName: props.pageName,
                        newData: { ...formData.value, ...props.otherInfo },
                    });
                }
            };

            // 关闭清空事件(这是方案2,上面的watch监听也可以实现清空)
            const clearContent = () => {
                // formData.value = {};
                // console.log('测试清空,但不执行哦');
            };

            return {
                dialogVisible,
                formData,
                handleConfirmClick,
                clearContent,
            };
        },
    });
</script>

<style scoped lang="less"></style>
View Code

 

2. 重难点剖析 

(1). page-modal组件中如何绑定对象监听ypf-form中的内容变化?

  通过v-model给ypf-form组件绑定一个空的formData,从而实现双向数据绑定。

 

(2). 如何实现新增弹框打开清空 和 编辑弹框打开回显 呢? 

 父组件打开新增 或 编辑 弹框的时候,在其方法里都会给defaultInfo赋值,新增传递的是 proxy{},编辑是 含内容的对象,都相当于defaultInfo发生了改变, 所以会触发这里watch监听,下面的newValue就是传递过来的新值;对于新增,空对象就相当于清空了;对于编辑有内容,相当于回显了。

 

(3). 如何实现新增 和 编辑 公用一个弹框的确认逻辑?

  通过判断defaultInfo有无值,来区分新增 or 编辑的确认逻辑,从而调用不同的接口

(4). 如何实现各个模块(如: 用户/角色) 通用的新增和编辑逻辑?

 A. 首先user.vue中调用page-modal的时候,需要传入pageName属性

 B. page-modal中将pageName属性传递到vuex中(另外需要吧 defaultInfo 和 otherInfo 合并也传递过去)

 C. vuex中拿到这个pageName,进行简单的封装调用service方法

 D. 最后需要接口也配合对应的规则即可

(5). 如何实现弹框中select类型的下拉框赋值?

 这个需要在父组件中实现,比如在user.vue中调用page-modal, 默认的model.config.js文件导入的对象需要处理一下,给其中的options属性赋值select下拉框中需要的内容,最后需要用computed包裹一下,然后在通过v-bind绑定给page-modal组件。 

(6). 如何实现新增显示密码框 编辑隐藏密码框呢?

  这个同样需要在父组件中实现,比如在user.vue中调用page-modal组件,新增 或者 编辑 的逻辑中,需要动态遍历到password所在的配置,将其isHidder属性对应的设置为true or  false。

 

 

 

 

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 

 

posted @ 2021-12-07 22:07  Yaopengfei  阅读(820)  评论(3编辑  收藏  举报