vue3 + ant 二次封装 form表单

1,656 阅读2分钟

# 此组件仅为个人无聊练手之用,其中或许还有些BUG和未完善的地方,求轻喷!

该表单基本功能如下: image.png 注:其中所有用到的组件皆为按需引入(但是Icon是全局引入的)

全部代码如下:

import { defineComponent, reactive, nextTick } from "vue"

// 按需引入组件
import { Form, FormItem, Input, Select, Radio, RadioGroup, Button, TreeSelect, Textarea, DatePicker, RangePicker, Upload, Switch, CheckboxGroup, Row, Col } from "ant-design-vue"

//引入组件所需类型
import type { Rule } from "ant-design-vue/es/form"
import type { DefaultOptionType } from "../../../node_modules/ant-design-vue/lib/vc-tree-select/TreeSelect"
import { RuleObject } from "ant-design-vue/lib/form"
import { UploadOutlined } from "@ant-design/icons-vue"
type Picker = import("../../../node_modules/ant-design-vue/lib/vc-picker/interface").PickerMode
type UploadRequestOption = import("../../../node_modules/ant-design-vue/lib/vc-upload/interface").UploadRequestOption
interface FormOptions {
    /** input */
    field: string // 字段名称
    label: string // 字段描述
    prefix?: Boolean //是否 input 的 左插槽
    suffix?: Boolean //是否 input 的 右插槽
    prefixIcon?: Element // input 的 左插槽的DOM
    suffixIcon?: Element // input 的 右插槽的DOM
    placeholder?: string // input框默认提示文字
    type: string // formItem 类型
    rule?: RuleObject[] // 表单校验规则

    /** select */
    mode?: "multiple" | "tags" | "combobox" | undefined // select 组件的mode
    showSelectSearch?: boolean
    filterSelectOption: (input: string, option: any) => boolean

    /** treeSelect */
    treeData?: any[] // 树形select 组件数据
    SHOW_PARENT?: string // 定义选中项回填的方式。TreeSelect.SHOW_ALL: 显示所有选中节点(包括父节点). TreeSelect.SHOW_PARENT: 只显示父节点(当父节点下所有子节点都选中时). 默认只显示子节点.
    filterTreeOption?: boolean | ((inputValue: string, treeNode: DefaultOptionType) => boolean) | undefined // 自定义树形选择组件的过滤方法
    searchValue?: string // 树形选择组件的搜索文字
    treeCheckble?: Boolean // 是否显示树形选择组件的checkBox

    /** time-picker */
    picker?: Picker // date | week | month | quarter | year 日期选择组件的类型

    /** upload */
    url: String // 文件上传路径
    customRequest?: (options: UploadRequestOption) => void // 文件自定义上传方法
    maxCount?: Number // 文件上传限制数量
    listType?: String // 文件上传类型限制
    multiple?: boolean // 是否支持多选 (按住ctrl)
    fileName: string // 发到后台的文件参数名

    /** switch */
    switchDisabled?: boolean
    switchSize?: "default" | "small" | undefined
    switchCheckedChildren?: Element | string | number
    switchUnCheckedChildren?: Element | string | number

    options?: { label: string; value: string | number; disabled?: boolean }[] // select/checkboxGroup 组件的数据
}
interface StrObj {
    [key: string]: any
}
type Layout = "horizontal" | "vertical" | "inline"

const FormCom = defineComponent({
    components: { Form, FormItem, Input, Radio, RadioGroup, TreeSelect, Textarea, DatePicker, RangePicker, Row, Col, Upload, Button, Switch, CheckboxGroup },
    props: {
        options: {
            type: Array,
            default: [],
        },
        rules: {
            type: Object,
            default: {},
        },
        id: {
            type: String,
            default: "",
        },
        footer: {
            type: Boolean,
            default: true,
        },
        formLayout: {
            type: String,
            default: "horizontal", // 'horizontal' | 'vertical' | 'inline';
        },
    },
    setup(props, { emit, slots }) {
        let formState: StrObj = reactive({})
        const option: FormOptions[] = props.options as FormOptions[]
        const rules: Record<string, Rule[]> = props.rules as Record<string, Rule[]>
        let dataInfo = reactive({}) as any
        const refFun = (e: any) => {
            dataInfo[`${props.id}`] = e
        }
        const changeInput = (e: Event, info: string) => {
            formState[info] = (e.target as HTMLInputElement).value
            console.log(formState, "formState")
        }
        const selectChange = (e: any, info: string) => {
            formState[info] = e
            console.log(e, formState, "selectChange")
        }
        const radioChange = (e: any, info: string) => {
            formState[info] = e.target.value
            console.log(e.target.vlaue, "radioChange")
        }
        const changeTreeSelect = (e: string, info: string) => {
            console.log(e, "form", formState)
            formState[info] = e
        }
        const searchChange = (e: string, info: string) => {
            console.log(e, "form", info)
            formState[info] = e
        }
        const panelChange = (e: any, info: string) => {
            formState[info] = e
        }
        const uploadChange = (e: any) => {
            console.log(e, "uploadChange")
            // e.fileList.forEach((item: any) => {
            // })
        }
        const switchChange = (e: any, info: string) => {
            formState[info] = e
            console.log(e, "switchChange")
        }
        const checkboxChange = (e: any, info: string) => {
            formState[info] = e
            console.log(e, "checkboxChange")
        }
        const filterTreeOption = (input: string, treeNode: StrObj) => {
            if (treeNode.value.includes(input)) return treeNode.value.includes(input)
            if (treeNode.title.includes(input)) return treeNode.title.includes(input)
        }
        const filterSelectOption = (input: string, option: any) => {
            return String(option.label).toLowerCase().indexOf(input.toLowerCase()) >= 0
        }
        const confirm = () => {
            nextTick(async () => {
                console.log(dataInfo[`${props.id}`].validateFields())
                await dataInfo[`${props.id}`]
                    .validate()
                    .then(() => {
                        console.log("success")
                        emit("confirm", formState)
                    })
                    .catch(() => {
                        console.log("error")
                    })
            })
        }
        const clickInputIcon = (e: any, type: string) => {
            emit("clickInputIcon", e, type)
        }
        const treeSelectItem = (title: string, i: string, fragment: string) => {
            if (typeof formState.searchValue == "string" && formState.searchValue !== "") {
                if (title.toLowerCase() == formState.searchValue.toLowerCase()) {
                    return <span style="color: aqua">{title}</span>
                } else {
                    return <div v-html={colorItem(title, formState.searchValue)}></div>
                }
            } else {
                return <span>{title}</span>
            }
        }
        const colorItem = (item: string, value: string) => {
            let reg = new RegExp(`${value}`, "gi")
            return item.replace(reg, `<span style="color:aqua">${value}</span>`)
        }
        const formElement = (item: FormOptions) => {
            if (item.type == "input") {
                return (
                    <Input
                        onChange={(e) => changeInput(e, item.field)}
                        placeholder={item.placeholder}
                        value={formState[item.field]}
                        v-slots={{
                            prefix: () => {
                                let PrefixIconItem = item.prefixIcon as any
                                let dom: JSX.Element[] | JSX.Element | Element | "" = item.prefix ? <PrefixIconItem onClick={(e: Event) => clickInputIcon(e, "prefix")} /> : ""
                                return dom
                            },
                            suffix: () => {
                                let SuffixIconItem = item.suffixIcon as any
                                let dom: JSX.Element[] | JSX.Element | "" = item.suffix ? <SuffixIconItem onClick={(e: Event) => clickInputIcon(e, "suffix")} /> : ""
                                return dom
                            },
                        }}
                    ></Input>
                )
            }
            if (item.type == "textarea") {
                return <Textarea onChange={(e) => changeInput(e, item.field)} placeholder={item.placeholder} value={formState[item.field]}></Textarea>
            }
            if (item.type == "select") {
                return (
                    <Select
                        value={formState[item.field]}
                        filterOption={(input: string, option: any) => {
                            return item.filterSelectOption ? item.filterSelectOption(input, option) : filterSelectOption(input, option)
                        }}
                        mode={item.mode as "multiple" | "tags" | "SECRET_COMBOBOX_MODE_DO_NOT_USE" | undefined}
                        style="width: 100%"
                        placeholder={item.placeholder}
                        show-search={item.showSelectSearch}
                        options={item.options}
                        onChange={(e) => selectChange(e, item.field)}
                    ></Select>
                )
            }
            if (item.type == "radioGroup") {
                return <RadioGroup value={formState[item.field]} options={item.options} onChange={(e) => radioChange(e, item.field)}></RadioGroup>
            }
            if (item.type == "treeSelect") {
                return (
                    <TreeSelect
                        onChange={(e: string) => changeTreeSelect(e, item.field)}
                        onSearch={(e: string) => searchChange(e, item.searchValue || "")}
                        value={formState[item.field]}
                        searchValue={formState[item.searchValue || ""]}
                        show-search
                        tree-default-expand-all
                        style="width: 100%"
                        tree-data={item.treeData}
                        tree-checkable={item.treeCheckble}
                        allow-clear
                        show-checked-strategy={item.SHOW_PARENT}
                        placeholder={item.placeholder}
                        filterTreeNode={item.filterTreeOption ? item.filterTreeOption : filterTreeOption}
                        v-slots={{
                            title: (info: { value: string; title: string }) => {
                                let dom: JSX.Element[] | JSX.Element = []
                                info.title
                                    .toString()
                                    .split(new RegExp(`(?<=${formState[item.searchValue || ""]})|(?=${formState[item.searchValue || ""]})`, "i"))
                                    .forEach((fragment: string, i: number) => {
                                        dom = treeSelectItem(info.title, String(i), fragment)
                                    })
                                return dom
                            },
                        }}
                    ></TreeSelect>
                )
            }
            if (item.type == "datePicker") {
                return <DatePicker placeholder={item.placeholder} picker={item.picker} value={formState[item.field]} onChange={(e) => panelChange(e, item.field)}></DatePicker>
            }
            if (item.type == "rangePicker") {
                return <RangePicker picker={item.picker} value={formState[item.field]} onChange={(e) => panelChange(e, item.field)}></RangePicker>
            }
            if (item.type == "upload") {
                return (
                    <Upload file-list={formState[item.field]} name={item.fileName} multiple={item.multiple} max-count={item.maxCount} list-type={item.listType} action={item.url as string} onChange={uploadChange} customRequest={item.customRequest}>
                        <Button>
                            <UploadOutlined></UploadOutlined>上传
                        </Button>
                    </Upload>
                )
            }
            if (item.type == "switch") {
                return (
                    <Switch
                        checked={formState[item.field]}
                        disabled={item.switchDisabled}
                        size={item.switchSize}
                        onChange={(e) => switchChange(e, item.field)}
                        v-slots={{
                            checkedChildren: (info: { value: string; title: string }) => {
                                let SwitchCheckedChildren = item.switchCheckedChildren as any
                                let dom: JSX.Element[] | JSX.Element | Element | "" = typeof SwitchCheckedChildren == "function" ? <SwitchCheckedChildren /> : <span>{item.switchCheckedChildren}</span>
                                return dom
                            },
                            unCheckedChildren: (info: { value: string; title: string }) => {
                                let SwitchUnCheckedChildren = item.switchUnCheckedChildren as any
                                let dom: JSX.Element[] | JSX.Element | Element | "" = typeof SwitchUnCheckedChildren == "function" ? <SwitchUnCheckedChildren /> : <span>{item.switchUnCheckedChildren}</span>
                                return dom
                            },
                        }}
                    ></Switch>
                )
            }
            if (item.type == "checkboxGroup") {
                return <CheckboxGroup onChange={(e) => checkboxChange(e, item.field)} value={formState[item.field]} options={item.options} />
            }
        }
        const formItemFun = () => {
            let renderDom: JSX.Element[] = []
            option.forEach((item) => {
                renderDom.push(
                    <FormItem required={item.rule ? item.rule[0].required : false} name={item.field} label={item.label}>
                        {formElement(item)}
                    </FormItem>
                )
            })
            return (
                <Form ref={(e) => refFun(e)} label-col={{ span: 4 }} wrapper-col={{ span: 14 }} name={`custom-validation${props.id ? "-" + props.id : ""}`} model={formState} rules={rules} layout={props.formLayout as Layout}>
                    {renderDom}
                    <div style={"width:100%;display:flex;justify-content: flex-end;"}>
                        <FormItem>
                            <Button style={"margin-right:20px;"} type="primary" onClick={confirm}>
                                确认
                            </Button>
                        </FormItem>
                        <FormItem>
                            <Button type="default" onClick={confirm}>
                                取消
                            </Button>
                        </FormItem>
                    </div>
                </Form>
            )
        }
        return () => formItemFun()
    },
})
export default FormCom

其中几个比较意思的点

1、TreeSelect组件的自定义过滤,将过滤后的数据进行高亮显示。

image.png 2、Input组件的左右插槽,Input组件插槽内的内容完全是由外部传进来,但是点击事件是由内部触发(ps:没阻止事件穿透)

image.png

Form表单使用方法

<template>
    <FormCom id="demoRef" :options="options" :rules="rules" @clickInputIcon="clickInputIcon" @confirm="confirm">
    </FormCom>
</template>
<script lang='ts'>
import { reactive, ref, toRefs, provide, defineComponent } from 'vue'
import FormCom from "./FormCom"
import type { FormProps } from "ant-design-vue"
import { UserAddOutlined, UserOutlined, InfoCircleOutlined } from "@ant-design/icons-vue"
interface Key {
    [key: string]: string
}
export default defineComponent({
    components: { FormCom, InfoCircleOutlined, UserOutlined, },
    setup(props: any, { emit }: any) {
        const filterTreeOption = (input: string, treeNode: Key) => {
            if (treeNode.value.includes(input)) return treeNode.value.includes(input)
            if (treeNode.title.includes(input)) return treeNode.title.includes(input)
        }
        const filterSelectOption = (input: string, option: any) => {
            return String(option.label).toLowerCase().indexOf(input.toLowerCase()) >= 0
        }
        let dataInfo = reactive({
            options: [
                {
                    field: "name",
                    label: "名称",
                    placeholder: "请输入名称",
                    type: "input",
                    prefix: true,
                    suffix: true,
                    prefixIcon: UserOutlined,
                    suffixIcon: InfoCircleOutlined
                },
                {
                    field: "text",
                    label: "备注",
                    placeholder: "请输入备注",
                    type: "textarea",
                },
                {
                    field: "age",
                    label: "年龄",
                    placeholder: "请输入年龄",
                    type: "input",
                },
                {
                    field: "sex",
                    label: "性别",
                    placeholder: "请选择性别",
                    type: "select",
                    showSelectSearch: true,
                    filterSelectOption: filterSelectOption,
                    options: [
                        { label: "男", value: 1 },
                        { label: "女", value: 2 },
                    ],
                    // mode:"multiple"
                },
                {
                    field: "job",
                    label: "工作",
                    placeholder: "请选择工作",
                    type: "radioGroup",
                    options: [
                        { label: "IT", value: 1 },
                        { label: "work", value: 2 },
                    ],
                }, {
                    field: "treeInfo",
                    label: "树选择",
                    placeholder: "请选择",
                    type: "treeSelect",
                    searchValue: "searchValue",
                    filterTreeOption: filterTreeOption,
                    treeCheckble: true,
                    treeData: [
                        {
                            title: 'Node1',
                            value: '0-0',
                            children: [
                                {
                                    title: 'Child Node1',
                                    value: '0-0-0',
                                },
                            ],
                        },
                        {
                            title: 'Node2',
                            value: '0-1',
                            children: [
                                {
                                    title: 'Child Node3',
                                    value: '0-1-0',
                                    disabled: true,
                                },
                                {
                                    title: 'Child Node4',
                                    value: '0-1-1',
                                },
                                {
                                    title: 'Child Node5',
                                    value: '0-1-2',
                                },
                            ],
                        },
                        {
                            title: 'tree',
                            value: '0-2',
                            children: [
                                {
                                    title: 'csgo',
                                    value: '0-2-1',
                                },
                            ],
                        },
                    ]
                },
                {
                    field: "time",
                    label: "日期",
                    placeholder: "请选择日期",
                    type: "datePicker",
                    picker: "date"
                },
                {
                    field: "timeAll",
                    label: "日期",
                    type: "rangePicker",
                    picker: "date"
                }, {
                    field: "picFile",
                    label: "图片",
                    type: "upload",
                    url: "localhost:2333"
                }, {
                    field: "switchValue",
                    label: "开关",
                    type: "switch",
                    switchCheckedChildren: "开",
                    switchUnCheckedChildren: "关",
                }, {
                    field: "check",
                    label: "多选",
                    type: "checkboxGroup",
                    options: [
                        { label: "IT", value: 1 },
                        { label: "work", value: 2 },
                        { label: "order", value: 3, disabled: true },
                    ],
                }
            ],
            rules: {
                name: [
                    {
                        required: true,
                        message: "请输入名称",
                        trigger: "blur",// change|blur
                    },
                ],
            },
        })
        const clickInputIcon = (e: any, type: string) => {
            console.log(e, type, "clickInputIcon");
        }
        const confirm = (e: any) => {
            console.log(e, "confirm");
        }
        return {
            ...toRefs(dataInfo),
            clickInputIcon,
            confirm,
        }
    },
})
</script>