第五節: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 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM