前期回顾
纯前端 —— 200行JS代码、实现导出Excel、支持DIY样式,纵横合并-CSDN博客https://blog.csdn.net/m0_57904695/article/details/135537511?spm=1001.2014.3001.5501
目录
一、🛠️ newTable.vue 封装Table
二、🚩 newForm.vue 封装搜索表单
三、📝 TS类型 src\types\global.d.ts
四、♻️ 页面使用功能 - 静态
五、♻️ 页面使用功能 - 动态
六、🤖 仓库地址、演示地址
七、📝 结语
在平时开发中,系统中写的最多的 表格+搜索表单排名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-tableref="multipleTableRef"v-bind="$attrs"stripestyle="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-columntype="selection"width="30"v-if="isSelection":reserve-selection="true":selectable="selectableCallback"/><el-table-columntype="index"label="序号"min-width="60":index="orderHandler"align="center"/><el-table-columnv-for="item in tableHeader"v-bind="item":key="item.prop"header-align="center"align="center"><template #header v-if="item.slotKey?.includes('tableHeaderSearch')"><el-inputv-model.trim="search"size="small":placeholder="getSearchInfo.label"/></template><template #default="{ row }" v-if="item.slotKey"><slotv-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-paginationv-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条 * 每页条数 + 当前索引+1return (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-inputv-if="item.type === FormOptionsType.INPUT"v-model.lazy.trim="searchForm[item.vm]"v-bind="item.props"class="ml10 w100"></el-input><el-selectv-if="item.type === FormOptionsType.SELECT"v-model.lazy="searchForm[item.vm]"v-bind="item.props"class="ml10 w100"fit-input-width><el-optionv-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-cascaderv-if="item.type === FormOptionsType.CASCADER"v-model.lazy="searchForm[item.vm]":options="item.cascaderOptions"v-bind="item.props"class="ml10 w100"/><el-date-pickerv-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-switchv-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-tablev-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-switchv-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',},],},],},],//这里允许动态属性所以可为空searchForm: <SearchFormType>{},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// if (searchForm.value.createTime) {// searchForm.value.startTime = searchForm.value.createTime[0];// searchForm.value.createTimeEnd = searchForm.value.createTime[1];// // delete searchForm.value.createTime;// }getTestList({pageNum,pageSize,...searchForm.value,}).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)
七、📝 结语
封装其他组件在其余博文中也有详细描写,快来抱走把!
_______________________________ 期待再见 _______________________________