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

9a69fede8b2044a79dd834e3e48f20b4.png前期回顾f8e3cc1a0f694ac2b665ca2ad14c49d7.png

纯前端 —— 200行JS代码、实现导出Excel、支持DIY样式,纵横合并-CSDN博客icon-default.png?t=N7T8https://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>

			<!-- 列表区域 v-bind="xx"放在最下方,父组件传值可以覆盖上面定义的默认值-->
			<!-- 父组件传递的属性(通过 $attrs 或显式传递的 prop)能够覆盖子组件内部的默认设置,你应该确保 v-bind 放在最后 -->
			<el-table
				ref="multipleTableRef"
				stripe
				style="width: 100%"
				:data="filterTableData"
				:border="tableBorder"
				:style="{
					height: tableHeight || excludeSearchAreaAfterTableHeight,
					minHeight: minHeight + 'px',
				}"
				:row-key="(row) => row.id"
				@row-click="handleRowClick"
				@selection-change="onSelectionChange"
				v-bind="$attrs"
			>
				<template #empty>
					<el-empty :image-size="emptyImgSize" description="暂无数据" />
				</template>

				<el-table-column
					v-if="isSelection"
					type="selection"
					width="60"
					:reserve-selection="true"
					:selectable="selectableCallback"
				/>
				<el-table-column v-if="isRadio" width="60" label="单选" align="center">
					<template #default="{ row }">
						<el-radio :label="row.id" v-model="selectRadioIdVm" />
					</template>
				</el-table-column>

				<el-table-column
					type="index"
					label="序号"
					min-width="60"
					:index="orderHandler"
					align="center"
				/>
				<el-table-column
					v-for="item in tableHeader"
					:key="item.prop"
					:fixed="item.label === '操作' ? 'right' : void 0"
					align="center"
					header-align="center"
					min-width="150"
					:show-overflow-tooltip="item.label !== '操作'"
					v-bind="item"
				>
					<template #header v-if="item.slotKey?.includes('tableHeaderSearch')">
						<el-input
							v-model.trim="headerSearch"
							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="Number(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 headerSearch = ref(''); //表头搜索Vm

const selectRadioIdVm = ref(''); // 单选框vm
const selectedRowData = ref({}); // 当前行数据
// 行点击事件-实现点击行也可以选中单选
const handleRowClick = (row: any) => {
	if (props.isRadio) {
		selectRadioIdVm.value = row.id; // 选中单选按钮
		selectedRowData.value = row; // 拿取当前行数据
		emits('selectRowData', selectedRowData.value);
	}
};

// 搜索过滤
const filterTableData = computed(() =>
	props.tableData?.filter(
		(data) =>
			!headerSearch.value ||
			String(data[getSearchInfo.value.prop])
				.toLowerCase()
				.includes(headerSearch.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);

// 根据父组件传递的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();
		}
	}
};
// 返回值用来决定这一行的 CheckBox 是否可以勾选
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();
const minHeight = 500; // 最小高度值

// 获取表格高度-动态计算搜索框高度
const updateHeight = () => {
	let wrapEl = document.querySelector('.scrollbar-height');
	if (!wrapEl) return;
	// 获取搜索区域高度
	Height.value = wrapEl.clientHeight;
	if (props.isShowSearchRegion) {
		const calculatedHeight = `calc(100vh - ${150 + Height.value}px)`;
		// 确保元素的高度不会小于一个最小值
		excludeSearchAreaAfterTableHeight.value = `max(${minHeight}px, ${calculatedHeight})`;
	}
};

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

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

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

	&-main {
		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-table tbody tr .cell) {
	padding-top: 10px;
	padding-bottom: 10px;
	/* 保留换行 
	 el-table-column打开了show-overflow-tooltip,换行不会生效
	*/
	// white-space: break-spaces;
}

// :deep(.el-popper.is-dark) {
// 	max-width: 700px !important;
// 	word-break: break-all;
// }

// 操作按钮间隙
:deep(.el-link) {
	padding-left: 10px;
}
/* 单选框隐藏label文字 */
:deep(.el-radio.el-radio--large .el-radio__label) {
	display: none;
}
</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
						@change="selectChange(item)"
					>
						<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', 'select-change']);
const debouncedEmitSearch = debounce((type) => emit('search', type));
const onSearch = (type: string) => {
	if (type) searchFormRef.value?.resetFields();
	debouncedEmitSearch(type);
};
const selectChange = (item: any) => {
	emit('select-change', item);
};

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 搜索字段-写在那个对象上,那个label 就有搜索,搜索展示在那个字段上。
	 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>

 六、✈️ 在封装(删除、列表请求)

import { ref, Ref } from 'vue';
import { ElMessage } from 'element-plus';
import { STATUS_CODE } from '/@/enum/global';

// 定义时间字段类型
type TimeFieldType = [string, string];

// 定义列表项数据类型
interface ListItem {
	[key: string]: any;
}

// 定义列表响应数据类型
interface ListResponse {
	code: STATUS_CODE;
	data: {
		list: ListItem[];
		total: string | number;
	};
}

// 定义搜索表单类型
interface SearchForm {
	[key: string]: any;
}

// 定义页面配置接口
interface PageConfig {
	pageNum: number;
	pageSize: number;
	total: number;
}
// 定义分页配置
const page = ref<PageConfig>({
	pageNum: 1,
	pageSize: 10,
	total: 0,
});
// 定义列表配置
interface UseTableListOptions {
	searchForm: Ref<SearchForm>;
	tableData: Ref<ListItem[]>;
	listAjaxFunction: (params: any) => Promise<any>;
	deleteAjaxFunction?: (id: number) => Promise<any>;
	extraParams?: Record<string, any>; // 给listAjaxFunction额外传参数
	timeFieldType?: TimeFieldType;
}
// 定义返回值类型
interface UseTableListReturns {
	onHandleSizeChange: (num: number) => void;
	onHandleCurrentChange: (sizes: number) => void;
	getTableList: (pageNum?: number, pageSize?: number, timeField?: TimeFieldType) => void;
	onSearch: (isReset?: boolean) => void;
	onHandleDelete: (id: number) => void;
	page: PageConfig;
}
/**
 * 抽离的list和del方法
 * @function useTableList
 * @param { Object } optionsPayload - 配置对象
 * @param { Ref<SearchForm> } optionsPayload.searchForm - 搜索表单VM绑定值
 * @param { Ref<ListItem[]> } optionsPayload.tableData - 表格数据
 * @param { Function  } optionsPayload.listAjaxFunction - 列表请求方法
 * @param { Function} [optionsPayload.deleteAjaxFunction] - 删除请求方法(可选)
 * @param { Record<string, any> } [optionsPayload.extraParams] - 额外的请求参数(可选)
 * @param { TimeFieldType } optionsPayload.timeFieldType - 时间字段类型
 * @returns { Object } 包含操作函数和响应式状态的对象
 * @author zk
 * @createDate 2024/01/24 19:10:12
 * @lastFixDate 2024/02/26 11:17
 **/
export function useTableList({
	searchForm,
	tableData,
	listAjaxFunction,
	deleteAjaxFunction,
	extraParams = {},
	timeFieldType = ['createTimeStart', 'createTimeEnd'],
}: UseTableListOptions): UseTableListReturns {
	// 页容量改变
	const onHandleSizeChange = (num: number) => {
		page.value.pageSize = num;
		getTableList(page.value.pageNum, num);
	};

	// 当前分页改变
	const onHandleCurrentChange = (sizes: number) => {
		page.value.pageNum = sizes;
		getTableList(sizes, page.value.pageSize);
	};

	// 获取表项数据
	const getTableList = (
		pageNum: number = 1,
		pageSize: number = 10,
		timeField: TimeFieldType = timeFieldType
	) => {
		let params: SearchForm = { ...searchForm.value, ...extraParams };
		// console.log('搜索表单VM绑定值', params);
		if (params.Time) {
			//有替换没有则新增
			params[timeField[0]] = params.Time[0];
			params[timeField[1]] = params.Time[1];
			delete params.Time;
		}
		listAjaxFunction({
			pageNum,
			pageSize,
			...params,
		}).then((res: ListResponse) => {
			if (res?.code !== STATUS_CODE.SUCCESS) return;
			const { list, total } = res.data;
			tableData.value = list;
			page.value.total = Number(total);
		});
	};

	// 搜索功能,支持重置
	const onSearch = (isReset?: boolean) => {
		page.value.pageNum = isReset ? 1 : page.value.pageNum;
		getTableList(page.value.pageNum, page.value.pageSize);
	};

	// 删除表项
	const onHandleDelete = (id: number) => {
		if (!deleteAjaxFunction) return;
		deleteAjaxFunction(id).then((res) => {
			if (res.code === STATUS_CODE.SUCCESS) {
				ElMessage.success('删除成功');
				onSearch();
			}
		});
	};

	return {
		onHandleSizeChange,
		onHandleCurrentChange,
		getTableList,
		onSearch,
		onHandleDelete,
		page: page.value,
	};
}

七、♻️ 使用 再次封装 的表格

     这里再次基于Element-plus Table二次封装后,再次封装列表和删除方法,方便使用、代码更少,编写速度再次推升

    只需要更改接口(列表、删除),即可直接使用功能

<template>
	<div class="container-wrapper">
		<!-- 数据安全威胁情报管理 page -->
		<new-table
			v-bind="state"
			@handleSizeChange="onHandleSizeChange"
			@handleCurrentChange="onHandleCurrentChange"
			@handleEdit="onEditDetail"
			@handleDelete="onHandleDelete"
		>
			<template #search>
				<new-form :formOptions="formOptions" :searchForm="searchForm" @search="onSearch" />
			</template>

			<template #btn>
				<el-button type="primary" @click="onAddDetail(DialogType.Add)" class="btn-add">
					<SvgIcon name="ant-PlusOutlined"></SvgIcon>
					新 增
				</el-button>
			</template>

			<template #detail="{ row }">
				<el-link
					type="primary"
					:underline="false"
					@click="onAddDetail(DialogType.Detail, row)"
					>详情</el-link
				>
			</template>
		</new-table>
		<Dialog ref="dialogRef" @on-success="onSearch" />
	</div>
</template>

<script setup lang="ts" name="intelligenceManagement">
import { onMounted, reactive, toRefs, defineAsyncComponent, ref } from 'vue';

import { querySecurityThreatList, deleteSecurityThreat } from '/@/api/knowledgeBase/backup';
import { useTableList } from '/@/utils/Hooks/tableSearch';
import { DialogType } from '/@/views/operationManage/operationMaintenance/syslogConfig/components/type';

const Dialog = defineAsyncComponent(() => import('./components/Dialog.vue'));
const dialogRef = ref<InstanceType<typeof Dialog> | null>(null);

const state = reactive({
	tableHeight: 'calc(100vh - 360px)',
	tableHeader: <TableHeader[]>[
		{ label: '标识号', prop: 'identifier' },
		{ label: '版本', prop: 'intelligenceVersion' },
		{ label: '威胁情报名称', prop: 'intelligenceName' },
		{ label: '威胁情报描述', prop: 'intelligenceDescription', minWidth: 200 },
		{ label: '创建时间', prop: 'createTime' },
		{ label: '操作', width: 200, slotKey: 'isEfficacy,detail,default' },
	],
	tableData: [],
	formOptions: <FormOptions[]>[
		{
			type: 'input',
			props: {
				label: '标识号',
				placeholder: '请输入标识号',
				type: 'text',
				clearable: true,
			},
			vm: 'identifier',
		},
		{
			type: 'input',
			props: {
				label: '版本',
				placeholder: '请输入版本',
				type: 'text',
				clearable: true,
			},
			vm: 'intelligenceVersion',
		},
		{
			type: 'input',
			props: {
				label: '威胁情报名称',
				placeholder: '请输入威胁情报名称',
				type: 'text',
				clearable: true,
			},
			vm: 'intelligenceName',
		},
		{
			type: 'date-picker',
			props: {
				label: '创建时间',
				type: 'datetimerange',
				clearable: true,
				'range-separator': '-',
				'start-placeholder': '开始日期',
				'end-placeholder': '结束日期',
				'value-format': 'YYYY-MM-DD HH:mm:ss',
			},
			vm: 'createTime',
		},
	],
	searchForm: <SearchFormType>{},
});

const { tableData, formOptions, searchForm } = toRefs(state);

const { onHandleSizeChange, onHandleCurrentChange, getTableList, onSearch, page, onHandleDelete } =
	useTableList({
		searchForm,
		tableData,
		listAjaxFunction: querySecurityThreatList,
		deleteAjaxFunction: deleteSecurityThreat,
		timeFieldType: ['startTime', 'endTime'],
	});

// 编辑
const onEditDetail = (row: any) => dialogRef.value?.open(DialogType.Edit, row);
// 新建、详情
const onAddDetail = (type: DialogType, row?: any) => dialogRef.value?.open(type, row);

onMounted(() => {
	getTableList();
	Object.assign(state, toRefs(page));
});
</script>

 

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

仓库地址:

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

 _______________________________  期待再见  _______________________________

  • 25
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
为了实现动态路由和用户权限路由,我们需要进行以下步骤: 1. 安装依赖 ```shell npm install vue-router@4 pinia element-plus mock -S npm install @types/mock -D ``` 2. 创建路由配置文件 在src文件夹下创建router文件夹,并在其创建index.ts文件,用于配置路由。在该文件,我们需要定义路由的各个页面组件,并根据需要配置路由的子路由和路由守卫。例如: ```typescript import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' import Home from '@/views/home/indexName.vue' import Login from '@/views/login/index.vue' import NotFound from '@/views/404/index.vue' const routes: Array<RouteRecordRaw> = [ { path: '/', name: 'Home', component: Home, meta: { title: '首页', requireAuth: true // 需要登录才能访问 } }, { path: '/login', name: 'Login', component: Login, meta: { title: '登录' } }, { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound, meta: { title: '404' } } ] const router = createRouter({ history: createWebHistory(), routes }) // 路由守卫 router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requireAuth && !token) { next('/login') } else { next() } }) export default router ``` 3. 在main.ts挂载路由 在main.ts,我们需要将路由配置文件挂载到Vue实例,以便在应用使用路由。例如: ```typescript import { createApp } from 'vue' import App from './App.vue' import router from './router' createApp(App).use(router).mount('#app') ``` 4. 创建权限控制文件 在src文件夹下创建permission文件夹,并在其创建index.ts文件,用于控制用户权限。在该文件,我们需要定义用户的权限列表,并根据需要判断用户是否有权限访问某个路由。例如: ```typescript import router from '@/router' const whiteList = ['/login'] // 不需要登录即可访问的页面 router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (token) { if (to.path === '/login') { next('/') } else { // 判断用户是否有权限访问该路由 const hasPermission = true // 根据实际情况判断用户是否有权限 if (hasPermission) { next() } else { next('/404') } } } else { if (whiteList.indexOf(to.path) !== -1) { next() } else { next('/login') } } }) ``` 5. 在App.vue挂载路由渲染入口 在App.vue,我们需要将路由渲染入口挂载到模板,以便在应用渲染路由。例如: ```html <template> <div id="app"> <router-view /> </div> </template> ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彩色之外

你的打赏是我创作的氮气加速动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值