0


Vue3 + TS + Element-Plus —— 项目系统中封装表格+搜索表单 十分钟写五个UI不在是问题

9a69fede8b2044a79dd834e3e48f20b4.png前期回顾f8e3cc1a0f694ac2b665ca2ad14c49d7.png

纯前端 —— 200行JS代码、实现导出Excel、支持DIY样式,纵横合并-CSDN博客https://blog.csdn.net/m0_57904695/article/details/135537511?spm=1001.2014.3001.5501

在平时开发中,系统中写的最多的 表格+搜索表单排名No.1,每一次都在Element-Plus中 拷贝一遍 <template> ,显然是个大活,我们将其html封装,每一只写Data数据让其动态渲染,编写速度达达滴!

一、🛠️ newTable.vue 封装Table

<template>
    <div class="container">
        <div class="container-main">
            <!-- 表单搜索区域 -->
            <el-scrollbar v-if="isShowSearchRegion" max-height="300px" class="scrollbar-height">
                <slot name="search"></slot>
            </el-scrollbar>

            <!-- 表格上方搜索向下方按钮区域 -->
            <slot name="btn"></slot>

            <!-- 列表区域 -->
            <el-table
                ref="multipleTableRef"
                v-bind="$attrs"
                stripe
                style="width: 100%"
                :data="filterTableData"
                :border="tableBorder"
                :height="tableHeight || excludeSearchAreaAfterTableHeight"
                :row-key="getRowKeys"
                @selection-change="onSelectionChange"
            >
                <template #empty>
                    <el-empty :image-size="emptyImgSize" description="暂无数据" />
                </template>

                <el-table-column
                    type="selection"
                    width="30"
                    v-if="isSelection"
                    :reserve-selection="true"
                    :selectable="selectableCallback"
                />

                <el-table-column
                    type="index"
                    label="序号"
                    min-width="60"
                    :index="orderHandler"
                    align="center"
                />

                <el-table-column
                    v-for="item in tableHeader"
                    v-bind="item"
                    :key="item.prop"
                    header-align="center"
                    align="center"
                >
                    <template #header v-if="item.slotKey?.includes('tableHeaderSearch')">
                        <el-input
                            v-model.trim="search"
                            size="small"
                            :placeholder="getSearchInfo.label"
                        />
                    </template>

                    <template #default="{ row }" v-if="item.slotKey">
                        <slot
                            v-for="slot in item.slotKey.split(',')"
                            :name="slot"
                            :row="row"
                        ></slot>
                        <template v-if="item.slotKey.includes('default')">
                            <el-link type="primary" :underline="false" @click="handleEdit(row)"
                                >编辑</el-link
                            >
                            <el-popconfirm title="确定删除吗?" @confirm="handleDelete(row.id)">
                                <template #reference>
                                    <el-link type="danger" :underline="false">删除</el-link>
                                </template>
                            </el-popconfirm>
                        </template>
                    </template>
                </el-table-column>
            </el-table>

            <!-- 分页区域-->
            <el-pagination
                v-if="paginationFlag"
                background
                :page-sizes="pageSizesArr"
                :current-page="pageNum"
                :page-size="pageSize"
                :layout="layout"
                :total="total"
                popper-class="pagination-popper"
                @size-change="handleSizeChange"
                @current-change="handleCurrentChange"
            ></el-pagination>
        </div>
    </div>
</template>

<script setup lang="ts">
import { onMounted, ref, watch, toRaw, nextTick, computed } from 'vue';
import { ElTable } from 'element-plus';
const multipleTableRef = ref<InstanceType<typeof ElTable>>();

import myEmits from './newTableConfig/emits';
import myProps from './newTableConfig/props';
const emits = defineEmits(myEmits);
const props = defineProps(myProps);
const search = ref('');

// 搜索过滤
const filterTableData = computed(() =>
    props.tableData?.filter(
        (data) =>
            !search.value ||
            String(data[getSearchInfo.value.prop])
                .toLowerCase()
                .includes(search.value.toLowerCase())
    )
);
// 计算那列用于展示搜索
const getSearchInfo = computed(() => {
    let searchInfo = { label: '', prop: '' };
    props.tableHeader?.find((v) => {
        if (v.searchFields) {
            searchInfo = { label: v.label, prop: v.prop };
            return true;
        }
    });
    return searchInfo;
});

// 序号根据数据长度计算
const orderHandler = (index: number) => {
    const { pageNum, pageSize } = props;
    // 第0条 * 每页条数 + 当前索引+1
    return (pageNum - 1) * pageSize + index + 1;
};

//  页数改变
const handleSizeChange = (val: number | string) => emits('handleSizeChange', val);
// 当前页改变
const handleCurrentChange = (val: number | string) => emits('handleCurrentChange', val);

// 编辑、删除
const handleEdit = (row: object) => emits('handleEdit', row);
const handleDelete = (id: number) => emits('handleDelete', id);
// 复选框
const onSelectionChange = (val: any) => emits('selection-table-change', val);

//记录每行的key值
const getRowKeys = (row: any) => row.id;

// 根据父组件传递的id字符串,默认选中对应行
const toggleSelection = (rows?: any) => {
    if (props.isSelection) {
        if (rows) {
            rows.forEach((row: any) => {
                const idsArr = props.selectionIds?.split(',');
                if (idsArr?.includes(row.id.toString())) {
                    //重要
                    nextTick(() => multipleTableRef.value?.toggleRowSelection(row, true));
                }
            });
        } else {
            multipleTableRef.value!.clearSelection();
        }
    }
};

const selectableCallback = (row: any) => {
    const idsArr = props.selectionIds?.split(',');
    if (props.isDisableSelection && idsArr?.includes(row.id.toString())) {
        return false;
    }
    return true;
};
watch(
    () => props.tableData,
    (newV) => {
        if (!!props.selectionIds) {
            // console.log('🤺🤺  selectionIds🚀 ==>:', props.selectionIds);
            // console.log('🤺🤺  newV ==>:', newV);
            toggleSelection(toRaw(newV));
        }
    }
);

// 搜索区域高度及默认值
const Height = ref();
// 减去搜索区域高度后的table,不能有默认值不然会出现滚动条
const excludeSearchAreaAfterTableHeight = ref();

// 获取表格高度-动态计算搜索框高度(onMounted、resize,208是已知的面包屑tebView高度)
const updateHeight = () => {
    let wrapEl = document.querySelector('.scrollbar-height') as HTMLElement | null;
    if (!wrapEl) return;
    Height.value = wrapEl.getBoundingClientRect().height;
    // console.log('🤺🤺  🚀 ==>:', wrapEl.getBoundingClientRect());
    if (props.isShowSearchRegion) {
        excludeSearchAreaAfterTableHeight.value = `calc(100vh - ${200 + Height.value}px)`;
    }
};

onMounted(() => {
    // 表格下拉动画
    const tableContainer = <HTMLElement>document.querySelector('.container');
    setTimeout(() => {
        if (tableContainer) tableContainer.style.transform = 'translateY(0)';
        updateHeight();
    }, 800);
});

window.addEventListener('resize', updateHeight);
defineExpose({
    toggleSelection,
});
</script>

<style scoped lang="scss">
.container {
    overflow: hidden;
    width: 100%;
    height: 100%;
    padding: 15px;
    transform: translateY(-100%);
    transition: transform 0.4s ease-in-out;
    background-color: #f8f8f8;
    // background-color: #870404;

    &-main {
        overflow: hidden;
        position: relative;
        padding: 15px;
        width: 100%;
        // height: 100%; el-scrollbar有默认高度100%,当页面列表渲前会继承这里高度,导致搜索区域铺满全屏
        background-color: #fff;
        border: 1px solid #e6e6e6;
        border-radius: 5px;
        &:hover {
            box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
        }
        transition: box-shadow 0.3s ease-in-out;
        .scrollbar-height {
            min-height: 100px;
        }

        .el-pagination {
            display: flex;
            align-items: center;
            justify-content: center;
            margin-top: 20px;
        }
    }
}
// 穿透父组件
:deep(.el-link) {
    padding-left: 10px;
}
</style>

二、🚩 newForm.vue 封装搜索表单

<template>
    <el-form ref="searchFormRef" :model="searchForm" size="default">
        <!-- 使用了不稳定的 key,可能会导致一些不可预期的行为,比如输入框失去焦点。 -->
        <el-row>
            <el-col
                :xs="24"
                :sm="24"
                :md="24"
                :lg="12"
                :xl="6"
                v-for="item in formOptions"
                :key="item.vm"
            >
                <el-form-item :label="item.props.label" :prop="item.vm">
                    <el-input
                        v-if="item.type === FormOptionsType.INPUT"
                        v-model.lazy.trim="searchForm[item.vm]"
                        v-bind="item.props"
                        class="ml10 w100"
                    ></el-input>

                    <el-select
                        v-if="item.type === FormOptionsType.SELECT"
                        v-model.lazy="searchForm[item.vm]"
                        v-bind="item.props"
                        class="ml10 w100"
                        fit-input-width
                    >
                        <el-option label="全部" value=""></el-option>

                        <el-option
                            v-for="option in item.selectOptions"
                            :key="option.value"
                            :label="option.label"
                            :value="option.value"
                        >
                            <zw-tooltip-omit :content="option.label"></zw-tooltip-omit>
                        </el-option>
                    </el-select>

                    <el-cascader
                        v-if="item.type === FormOptionsType.CASCADER"
                        v-model.lazy="searchForm[item.vm]"
                        :options="item.cascaderOptions"
                        v-bind="item.props"
                        class="ml10 w100"
                    />

                    <el-date-picker
                        v-if="item.type === FormOptionsType.DATE_PICKER"
                        v-model.lazy="searchForm[item.vm]"
                        v-bind="item.props"
                        class="ml10 w100"
                    />
                </el-form-item>
            </el-col>
            <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="6" class="xs-mt">
                <el-form-item style="margin-left: 10px">
                    <el-button @click="onSearch('reset')">
                        <SvgIcon name="ant-ReloadOutlined"></SvgIcon>
                        重置
                    </el-button>
                    <el-button type="primary" @click="onSearch()">
                        <SvgIcon name="ant-SearchOutlined"></SvgIcon>
                        查询
                    </el-button>
                </el-form-item>
            </el-col>
        </el-row>
    </el-form>
</template>

<script setup lang="ts" name="newForm">
import { toRefs, onBeforeUnmount, ref } from 'vue';
import type { PropType } from 'vue';
import { type FormInstance } from 'element-plus';
import { debounce } from '/@/utils/debounce';
const searchFormRef = ref<FormInstance>();

enum FormOptionsType {
    INPUT = 'input', // 输入框
    SELECT = 'select', // 下拉框
    CASCADER = 'cascader', // 级联选择器
    DATE_PICKER = 'date-picker', // 日期选择器
}

const props = defineProps({
    formOptions: {
        type: Array as PropType<FormOptions[]>,
        required: true,
    },
    searchForm: {
        type: Object as PropType<SearchFormType>,
        required: true,
    },
});
const { formOptions, searchForm } = toRefs(props);

const emit = defineEmits(['search']);
const debouncedEmitSearch = debounce((type) => emit('search', type));
const onSearch = (type: string) => {
    if (type) searchFormRef.value?.resetFields();
    debouncedEmitSearch(type);
};

onBeforeUnmount(() => searchFormRef.value?.resetFields());
defineExpose({ searchFormRef });
</script>

<style scoped lang="scss">
:deep(.el-form-item__label) {
    margin-left: 10px;
}
</style>

<style scoped lang="scss">
:deep(.el-form-item__label) {
    margin-left: 10px;
}
</style>

三、📝 TS类型 src\types\global.d.ts


// new-table
//表头数据类型定义
declare interface TableHeader<T = any> {
    label: string;
    prop: string;
    align?: string;
    overHidden?: boolean;
    minWidth?: string;
    sortable?: boolean;
    type?: string;
    fixed?: string;
    width?: string | number;
    // isActionColumn?: boolean; // 是否是操作列
    // isCustomizeColumn?: boolean; // 是否是自定义列
    slotKey?: string; // 自定义列的插槽名称
    searchFields?: boolean; // 是否是搜索字段
}

/*
  newForm
 允许任何字符串作为索引
 不然会报错, 使用动态属性名,需要使用索引签名
*/
declare type SearchFormType = {
    [key: string]: string;
};

declare type FormOptions = {
    type: string;
    props: {
        label: string;
        placeholder: string;
        type: string;
        clearable: boolean;
    };
    vm: string;
    selectOptions?: {
        value: string | number;
        label: string;
    }[];
    cascaderOptions?: any;
};

四、♻️ 页面使用功能 - 静态

<template>
    <new-table
        :tableHeader="tableHeader"
        :tableData="tableData"
        :total="100"
        @handleSizeChange="onHandleSizeChange"
        @handleCurrentChange="onHandleCurrentChange"
        @handleDelete="onHandleDelete"
        @handleEdit="onHandleEdit"
    >
        <template #search>
            <el-row>
                <el-col
                    :xs="24"
                    :sm="24"
                    :md="24"
                    :lg="12"
                    :xl="6"
                    v-for="item in Math.ceil(Math.random() * 10)"
                    :key="item"
                    class="scrollbar-demo-item"
                    >56546</el-col
                >
                <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="6" class="xs-mt">
                    <el-form-item>
                        <el-button> 重置 </el-button>
                        <el-button type="primary"> 查询 </el-button>
                    </el-form-item>
                </el-col>
            </el-row>
        </template>

        <template #switch="slotProps">
            <el-switch
                v-model="slotProps.row.status"
                active-text="开"
                inactive-text="关"
                active-color="#13ce66"
                inactive-color="#ff4949"
                @change="changeSwitchStatus(slotProps.row.id, slotProps.row.status)"
            />
        </template>
    </new-table>
</template>

<script setup lang="ts" name="algorithmRegistrationQuery">
import { reactive, toRefs } from "vue";
const state = reactive({
    //表头数据
    tableHeader: <TableHeader[]>[
        { label: "姓名", prop: "uname", width: "100px" },
        { label: "年龄", prop: "age", slotKey: "switch" },
        { label: "性别", prop: "sex" },
        { label: "操作", width: "240px", fixed: "right", slotKey: "default" },
    ],

    //表数据,从接口获取
    tableData: [
        { uname: "小帅", age: "18", sex: "男", status: false, id: 1 },
        { uname: "小美", age: "148", sex: "女", status: false, id: 2 },
        { uname: "小明", age: "12", sex: "男", status: true, id: 3 },
        { uname: "小红", age: "12", sex: "女", status: false, id: 4 },
        { uname: "小黑", age: "12", sex: "男", status: true, id: 5 },
        { uname: "小白", age: "12", sex: "女", status: false, id: 6 },
        { uname: "小黑", age: "12", sex: "男", status: true, id: 7 },
        { uname: "小白", age: "12", sex: "女", status: false, id: 8 },
        { uname: "小黑", age: "12", sex: "男", status: true, id: 9 },
        { uname: "小白", age: "12", sex: "女", status: false, id: 10 },
        { uname: "小黑", age: "12", sex: "男", status: true, id: 11 },
    ],
});
const { tableHeader, tableData } = toRefs(state);

// 修改
const onHandleEdit = (row: object) => {
    console.log(row);
};

// 删除
const onHandleDelete = (row: object) => {
    console.log(row);
};

// switch
const changeSwitchStatus = (id: number, status: boolean) => {
    console.log(id, status);
};

//分页改变
const onHandleSizeChange = (val: number) => {
    console.log("!这里输出 🚀 ==>:", val);
};
//分页改变
const onHandleCurrentChange = (val: number) => {
    console.log("!这里输出 🚀 ==>:", val);
};

// //页容量改变
// const onHandleSizeChange = (val: number) => {
//     // console.log('页容量 ==>:', val);
//     pageSize.value = val;
//     getTableList(pageNum.value, pageSize.value, tableId.value);
// };
// //当前分页改变
// const onHandleCurrentChange = (val: number) => {
//     // console.log('当前页 🚀 ==>:', val);
//     pageNum.value = val;
//     getTableList(pageNum.value, pageSize.value, tableId.value);
// };
</script>

<style lang="scss" scoped>
.scrollbar-demo-item {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 50px;
    margin: 10px;
    text-align: center;
    border-radius: 4px;
    background: var(--el-color-primary-light-9);
    color: var(--el-color-primary);
}
.xs-mt {
    display: flex;
    align-items: flex-end;
}
</style>

五、♻️ 页面使用功能 - 动态

<template>
    <div class="container-wrapper">
        <!-- 动态 page -->
        <new-table
            v-bind="state"
            :total="pageTotal"
            @handleSizeChange="onHandleSizeChange"
            @handleCurrentChange="onHandleCurrentChange"
            @handleEdit="onHandleEdit"
            @handleDelete="onHandleDelete"
        >
            <template #search>
                <new-form :formOptions="formOptions" :searchForm="searchForm" @search="onSearch" />
            </template>

            <template #btn>
                <el-button type="primary" size="default" class="btn-add">
                    <SvgIcon name="ant-PlusOutlined"></SvgIcon>
                    新建题目
                </el-button>
            </template>

            <template #switch="{ row }">
                <el-switch
                    v-model="row.fileStatus"
                    active-text="开"
                    inactive-text="关"
                    :active-value="1"
                    :inactive-value="2"
                    active-color="#13ce66"
                    inactive-color="#ff4949"
                    @change="changeSwitchStatus(row.id, row.fileStatus)"
                />
            </template>
        </new-table>
    </div>
</template>

<script setup lang="ts" name="algorithmRegistrationQuery">
import { onMounted, reactive, toRefs } from 'vue';
import { getTestList } from '/@/api/encryptionAlgorithm/templateDefinition';
import { STATUS_CODE } from '/@/enum/global';
const state = reactive({
    //表头数据
    // el-table-column有的属性都可以在这传

    /* 
     searchFields:true 搜索字段
     slotKey: 'xxx' 自定义插槽 
     包含tableHeaderSearch则展示表格搜索框。
     包含default则展示 编辑删除
     其他值可以在父组件中使用插槽 template自定义内容
      #search 表单搜索
      #btn 列表上方的按钮
    */
    tableHeader: <TableHeader[]>[
        { label: '合规规则', prop: 'knowledgeName', searchFields: true },
        { label: '文件数量', prop: 'documentNumber', width: '200px' },
        { label: '文件状态', prop: 'fileStatus', slotKey: 'switch' },
        { label: '操作', fixed: 'right', slotKey: 'default,tableHeaderSearch', width: 200 },
    ],
    //表项数据
    tableData: [],
    formOptions: <FormOptions[]>[
        {
            type: 'input',
            props: {
                label: '合规规则',
                placeholder: '请输入合规规则',
                type: 'text',
                clearable: true,
            },
            vm: 'knowledgeName',
        },
        {
            type: 'input',
            props: {
                label: '文件数量',
                placeholder: '请输入文件数量',
                type: 'text',
                clearable: true,
            },
            vm: 'documentNumber',
        },
        // 下拉选择器
        {
            type: 'select',
            props: {
                label: '所属部门',
                placeholder: '请选择',
                clearable: true,
            },
            vm: 'department',
            selectOptions: [
                {
                    label: '数据安全',
                    value: 1,
                },
                {
                    label: '研发',
                    value: 2,
                },
                {
                    label: '事业',
                    value: 3,
                },
            ],
        },
        // 时间范围选择器
        {
            type: 'date-picker',
            props: {
                label: '时间范围',
                type: 'datetimerange', // datetimerange范围 datetime日期
                clearable: true,
                'range-separator': '-',
                'start-placeholder': '开始日期',
                'end-placeholder': '结束日期',
                'value-format': 'YYYY-MM-DD HH:mm:ss',
            },
            vm: 'createTime',
        },

        // 级联选择器
        {
            type: 'cascader',
            props: {
                label: '所属部门',
                placeholder: '请选择',
                clearable: true,
            },
            vm: 'cascader',
            cascaderOptions: [
                {
                    value: 'guide',
                    label: 'Guide',
                    children: [
                        {
                            value: 'disciplines',
                            label: 'Disciplines',
                            children: [
                                {
                                    value: 'consistency',
                                    label: 'Consistency',
                                },
                            ],
                        },
                        {
                            value: 'navigation',
                            label: 'Navigation',
                            children: [
                                {
                                    value: 'side nav',
                                    label: 'Side Navigation',
                                },
                                {
                                    value: 'top nav',
                                    label: 'Top Navigation',
                                },
                            ],
                        },
                    ],
                },
                {
                    value: 'component',
                    label: 'Component',
                    children: [
                        {
                            value: 'basic',
                            label: 'Basic',
                            children: [
                                {
                                    value: 'button',
                                    label: 'Button',
                                },
                            ],
                        },
                        {
                            value: 'form',
                            label: 'Form',
                            children: [
                                {
                                    value: 'radio',
                                    label: 'Radio',
                                },
                                {
                                    value: 'checkbox',
                                    label: 'Checkbox',
                                },
                            ],
                        },
                        {
                            value: 'data',
                            label: 'Data',
                            children: [
                                {
                                    value: 'table',
                                    label: 'Table',
                                },
                            ],
                        },
                        {
                            value: 'notice',
                            label: 'Notice',
                            children: [
                                {
                                    value: 'alert',
                                    label: 'Alert',
                                },
                            ],
                        },
                        {
                            value: 'navigation',
                            label: 'Navigation',
                            children: [
                                {
                                    value: 'menu',
                                    label: 'Menu',
                                },
                            ],
                        },
                        {
                            value: 'others',
                            label: 'Others',
                            children: [
                                {
                                    value: 'dialog',
                                    label: 'Dialog',
                                },
                            ],
                        },
                    ],
                },
                {
                    value: 'resource',
                    label: 'Resource',
                    children: [
                        {
                            value: 'axure',
                            label: 'Axure Components',
                        },
                    ],
                },
            ],
        },
    ],
    //这里允许动态属性所以可为空,如果是下拉选项将vm置为空就会匹配到子组件的'全部'label字段
    searchForm: <SearchFormType>{
        department: '',
    },
    pageNum: 1,
    pageSize: 10,
    pageTotal: 0,
    tableHeight: 'calc(100vh - 375px)', //如果开启#btn占位符需要手动设置表格高度
});
const { tableData, formOptions, searchForm, pageNum, pageSize, pageTotal } = toRefs(state);

// 修改
const onHandleEdit = (row: object) => {
    console.log(row);
};

// 删除
const onHandleDelete = (row: object) => {
    console.log(row);
};

// switch
const changeSwitchStatus = (id: number, status: boolean) => {
    console.log(id, status);
};

//页容量改变
const onHandleSizeChange = (val: number) => {
    // console.log('页容量 ==>:', val);
    pageSize.value = val;
    getTableList(pageNum.value, pageSize.value);
};
//当前分页改变
const onHandleCurrentChange = (val: number) => {
    // console.log('当前页 🚀 ==>:', val);
    pageNum.value = val;
    getTableList(pageNum.value, pageSize.value);
};

// 获取表项数据
const getTableList = (pageNum: number, pageSize: number) => {
    // 处理searchForm.value createTime
    let params = { ...searchForm.value };
    if (params.createTime) {
        params.createTimeBegin = params.createTime[0];
        params.createTimeEnd = params.createTime[1];
    }
    // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
    const { createTime, ...paramsWithoutCreateTime } = params;
    getTestList({
        pageNum,
        pageSize,
        ...paramsWithoutCreateTime,
    }).then((res) => {
        if (res.code !== STATUS_CODE.SUCCESS) return;
        const { list, total } = res.data;
        tableData.value = list;
        // console.log('🤺🤺 表项 🚀 ==>:', list);
        pageTotal.value = total;
    });
};

const onSearch = (isReset?: string) => {
    pageNum.value = isReset ? 1 : pageNum.value;
    getTableList(pageNum.value, pageSize.value);
};

onMounted(() => getTableList(pageNum.value, pageSize.value));
</script>

<style scoped lang="scss">
.btn-add {
    float: right;
    margin-bottom: 20px;
}
</style>

六、🤖 仓库地址、演示地址

仓库地址:

Vite + Ts + Vue3 - template -- 模板: 🎉🎉🔥 Vite + Vue3 + Ts + router + Vuex + axios + eslint 、prettier、stylelint、husky、gitCommit --- 集成多种组件、Hooks支持开封即用,严格的代码质量检验、祝您轻松上大分😷🤺🤺🤺 【动态路由、特效、N个组件、N个自定义指令...】 (gitee.com)

在线演示:

Vite + Vue + TS (gitee.io)

七、📝 结语

封装其他组件在其余博文中也有详细描写,快来抱走把!

7730e2bd39d64179909767e1967da702.jpeg

** _______________________________ 期待再见 ** _______________________________


本文转载自: https://blog.csdn.net/m0_57904695/article/details/135538630
版权归原作者 彩色之外 所有, 如有侵权,请联系我们删除。

“Vue3 + TS + Element-Plus —— 项目系统中封装表格+搜索表单 十分钟写五个UI不在是问题”的评论:

还没有评论