为了偷偷的偷懒,封个curd hook【vue3、element-plus】

253 阅读1分钟

最终用法

<template>
    <div class="zn-content">
        <div class="filter-container">
            <el-select class="filter-item"></el-select>
            <el-input v-model.trim="tableQuery.name" class="filter-item" placeholder="" clearable @clear="handleSearch" @keyup.enter="handleSearch"> </el-input>
            <el-button class="filter-item" type="primary" @click="handleSearch">查询</el-button>
            <el-button class="filter-item" @click="reset">重置</el-button>
        </div>
    <div class="handle-container">
        <el-button type="primary" @click="handleCreate">新增</el-button>
    </div>
    <Table showIndex>
        <el-table-column label="" prop="" show-overflow-tooltip></el-table-column>
        <el-table-column label="操作" align="center" fixed="right" width="140" #default="{ row }">
            <el-button link type="primary" @click="handleUpdateBefore(row)">修改</el-button>
            <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
        </el-table-column>
    </Table>
    <zy-drawer :title="dialogTitleMap[dialogType]" v-model="dialogVisible" :okProps="{ loading: handleLoading }" @ok="handleConfirm">
        <el-form ref="formRef" :model="temp" label-width="84">
            <el-form-item label="姓名" prop="name" :rules="required">
                <el-input v-model="temp.name" placeholder="请输入姓名"></el-input>
            </el-form-item>
            <el-form-item label="身份证号" prop="idCard" :rules="[required, validatorFun.idCard]">
                <el-input v-model="temp.idCard" placeholder="请输入身份证号"></el-input>
            </el-form-item>
            <el-form-item label="政治面貌" prop="politics" :rules="required">
                <dict-select v-model="temp.politics" class="w-100" code="PS" placeholder="请选择政治面貌" />
            </el-form-item>
            <el-form-item label="手机号" prop="telephone" :rules="[required, validatorFun.phone]">
                <el-input v-model="temp.telephone" placeholder="请输入手机号"></el-input>
            </el-form-item>
            <el-form-item label="工作性质" prop="jobNature" :rules="required">
                <dict-select v-model="temp.jobNature" class="w-100" code="JOB" placeholder="请选择工作性质" />
            <el-form-item label="角色" prop="roleId" :rules="required">
                <scroll-select-v2 v-model="temp.roleId" class="w-100" placeholder="请选择或搜索角色" :api="roleListApi"></scroll-select-v2>
            </el-form-item>
            <el-form-item label="照片" prop="attachInfoList" :rules="required">
                <zy-upload v-model="temp.attachInfoList" :limit="1"></zy-upload>
            </el-form-item>       
        </el-form>
    </zy-drawer>
	</div>
</template>

<!--  -->
<script setup>
import { useTable } from "@/hooks/useTable";
import ScrollSelectV2 from "@/components/scroll-select-v2/index.vue";
import DictSelect from "@/components/dict-select/index.vue";
import { required, validatorFun } from "@/utils/formValid";
import { roleListApi } from "@/api/common";

const apiOpt = {
	list: "", // 列表接口
	create: "", // 新增接口
	update: "", // 修改接口
	del: "", // 删除接口
	detail: "" // 详情接口
};
const {
	Table,
	tableRef, // 表格实例
	formRef, // 表单实例
	tableQuery,
	temp,
	tableData,
	dialogTitleMap,
	dialogType,
	dialogVisible,
	handleLoading,
	handleSearch,
	handleCreate,
	handleConfirm,
	handleUpdateBefore,
	handleDelete,
	reset
} = useTable({ apiOpt, methods: { getTableData } });

// 假如要覆盖该方法
function getTableData() {
	tableData.value = [{ id: 1 }];
}
</script>

表格二次封装 src/components/zy-table/index.vue

<template>
    <div v-loading="loading" class="zn-table-content" :style="{ height }">
        <el-table ref="tableRef" class="flex-1" :class="{ 'hide-checkAll': hideDftCheckAll && !headSelect }" row-key="id" :data="data" @selection-change="handleSelectionChange" v-bind="$attrs">
            <el-table-column v-if="showSelect" type="selection" reserve-selection width="55" align="center">
                <template #default="scope">
                    <slot name="select" v-bind="scope">
                        <el-checkbox :disabled="selectDisabled && selectDisabled(scope.row)"
                              :model-value="transferCheckedStatus(scope.store, scope.row)"
							@change="scope.store.toggleRowSelection(scope.row)"
						></el-checkbox>
					</slot>
				</template>
			</el-table-column>
			<el-table-column v-if="showIndex" label="序号" type="index" align="center" width="70">
				<template #default="{ $index, row }">{{ indexFormat($index, row) }}</template>
			</el-table-column>
			<slot></slot>
			<template v-if="customEmpty" #empty>
				<div>
					<el-empty v-if="!loading" style="width: 100%; height: 100%" />
				</div>
			</template>
			<template #append>
				<slot name="append"> </slot>
				<div v-if="data?.length && showBatchDel" class="flex al-center p-y-8 m-b-8">
					<div v-if="showSelect" style="width: 55px" class="flex ju-center m-r-28">
						<el-checkbox :model-value="checkAll" :indeterminate="indeterminate" class="table-check-all" @change="handleCheckAll"></el-checkbox>
					</div>
					<slot name="batch-action" :selection="multipleSelection">
						<el-button :disabled="!multipleSelection?.length" plain type="danger" @click="handleBatchDel">批量删除</el-button>
					</slot>
				</div>
			</template>
		</el-table>
		<el-table v-if="summaryData?.length" :data="summaryData" :show-header="false">
			<el-table-column v-if="showSelect" width="55"></el-table-column>
			<el-table-column v-if="showIndex" width="60"></el-table-column>
			<slot name="summary-columns">
				<slot></slot>
			</slot>
			<template #empty>
				<i></i>
			</template>
		</el-table>
		<div v-show="data?.length" class="flex al-center">
			<div v-if="showSelect && !headSelect && ((hideDftCheckAll && !showBatchDel) || $slots.footer)" style="width: 55px" class="flex ju-center m-r-28 m-t-24">
				<el-checkbox :model-value="checkAll" :indeterminate="indeterminate" class="table-check-all" @change="handleCheckAll"></el-checkbox>
			</div>
			<div v-if="$slots.footer" class="m-t-24">
				<slot name="footer" :selection="multipleSelection"></slot>
			</div>
			<el-pagination
				v-if="showP"
				class="table-pagination m-t-24"
				:currentPage="tableQuery?.pageIndex"
				:page-size="tableQuery?.pageSize"
				:page-sizes="[20, 30, 50, 100, 200]"
				layout="total, sizes, prev, pager, next, jumper"
				:total="total ?? 0"
				@size-change="(v) => emit('handleSizeChange', v)"
				@current-change="(v) => emit('handleCurrentChange', v)"
			/>
		</div>
	</div>
</template>
<script setup>
import { ref, watch } from "vue";

const props = defineProps({
	tableQuery: { type: Object, default: () => ({ pageIndex: 1, pageSize: 20 }) },
	data: Array,
	summaryData: Array,
	total: Number,
	height: String,
	showIndex: { type: Boolean, default: false }, // 是否显示序号列
	showSelect: { type: Boolean, default: false }, // 是否显示checkbox
	selectDisabled: Function, // 行选项checkbox禁用条件
	selectDft: Function, // 行默认选中条件
	headSelect: { type: Boolean, default: false },
	hideDftCheckAll: { type: Boolean, default: true },
	showP: {
		// 是否显示页码
		type: Boolean,
		default: true
	},
	showBatchDel: { type: Boolean, default: false }, // 是否显示批量删除
	loading: { type: Boolean, default: false },
	customEmpty: { type: Boolean, default: true } // 是否显示自定义空数据
});
const tableRef = ref();
const emit = defineEmits(["handleSizeChange", "handleCurrentChange", "handleBatchDel", "handleSeleted"]);
watch(
	() => props.tableQuery,
	() => {
		tableRef.value?.setScrollTop(0);
	},
	{ deep: true }
);
const multipleSelection = ref([]);

// 多选
function handleSelectionChange(val) {
	multipleSelection.value = val;
	indeterminate.value = val.length > 0 && val.length < props.data?.length;
	checkAll.value = props.data?.length > 0 && val.length === props.data?.length;
	emit("handleSeleted", multipleSelection.value);
}
// 批量删除
function handleBatchDel() {
	emit("handleBatchDel", multipleSelection.value);
}
// 序号格式化
function indexFormat(index, row) {
	if (row.rowIndex) {
		index = row.rowIndex;
	} else if (row.rowIndex === false) {
		return "";
	}
	const { pageIndex = 1, pageSize = 20 } = props.tableQuery;
	return index + 1 + (pageIndex - 1) * pageSize;
}

const indeterminate = ref(false);
const checkAll = ref(false);
function handleCheckAll() {
	tableRef.value.toggleAllSelection();
}

// 弄选中状态
function transferCheckedStatus(store, row) {
	const disabled = props.selectDisabled && props.selectDisabled(row);
	if (disabled && store?.isSelected(row)) {
		store.toggleRowSelection(row);
	}
	store.updateAllSelected();
	return store?.isSelected(row);
}
defineExpose({ handleCheckAll, tableInstance: tableRef });
</script>
<style lang="scss">
.zn-table-content {
	height: 100%;
	display: flex;
	flex: 1;
	flex-direction: column;
	overflow: auto;

	.hide-checkAll {
		thead {
			.el-table-column--selection {
				.el-checkbox {
					visibility: hidden;
				}
			}
		}
	}

	.table-check-all {
		position: relative;

		&::after {
			position: absolute;
			content: "全选";
			transform: translateX(calc(50% + 8px));
		}
	}

	.table-pagination {
		margin-left: auto;
	}

	.el-button {
		&.is-text {
			+ .el-button {
				margin-left: 0;
			}
		}
	}
}
</style>

useTable src/hooks/useTable.js

import _Table from "@/components/zy-table/index.vue";
import { ElLoading, ElMessage, ElMessageBox } from "element-plus";
import { downloadFile, obj2Formdata, selectLocalFile, typeCheck } from "@/utils/utils";
import { fileType } from "@/constant";
import debounce from "@/utils/debounce";
import { ref, reactive, toRefs, onBeforeMount, h } from "vue";

export const dialogTypeOpt = {
	create: "create",
	update: "update",
	detail: "detail"
};

/**
 * @param {{
 * 	data:{
 * 		[string]:any
 * 	} // 需要初始化的变量
 * 	methods:{
 * 		[string]:Function
 * 	} // 需要覆盖的方法
 * 	apiOpt:{
 * 		detailIdKey:string // 详情id键值
 * 		list:Promise // 列表接口
 * 		create:Promise // 创建接口
 * 		del:Promise // 删除接口
 * 		detail:Promise // 详情接口
 * 		update:Promise // 修改接口
 * 		import:Promise // 导入接口
 * 		download:Promise // 导出接口
 * }
 * }} param
 * @returns
 */
export const useTable = ({ data, apiOpt, methods } = {}) => {
	const formRef = ref(), // 表单实例
		tableRef = ref(); // 表格实例
	const _data = reactive({
		tableData: [],
		tableQuery: {
			pageIndex: 1,
			pageSize: 20
		},
		temp: {},
		dialogVisible: false,
		total: 0,
		tableLoading: false,
		handleLoading: false,
		dialogTypeOpt,
		dialogType: "",
		dialogTitleMap: {
			[dialogTypeOpt.create]: "新增",
			[dialogTypeOpt.update]: "修改",
			[dialogTypeOpt.detail]: "详情"
		},
		fileName: null,
		...data
	});
	let debounceFun;
	onBeforeMount(() => {
		_methods.fetchData();
		_methods.getTableData();
	});
	const _methods = {
		handleSearch() {
			_data.tableQuery.pageIndex = 1;
			_data.total = 0;
			tableRef.value?.tableInstance?.clearSelection();
			if (!debounceFun) {
				debounceFun = debounce(_methods.getTableData, 0);
			}
			debounceFun();
		},
		showConfirm(cb) {
			ElMessageBox.confirm("是否确定此操作?", "系统提示", {
				type: "warning",
				autofocus: false
			})
				.then(cb)
				.catch();
		},
		// 查
		async getTableData() {
			if (apiOpt?.list) {
				const query = _methods.getTableQuery();
				if (query === false) {
					_data.total = 0;
					_data.tableData = [];
					return;
				}
				_data.tableLoading = true;
				const response = await apiOpt.list(query);
				_methods.getTableDataNormalSuccess(response);
			} else {
				console.log("未定义list接口或getTableData方法");
			}
		},
		getTableQuery() {
			return _data.tableQuery;
		},
		// 校验并操作
		handleConfirm() {
			formRef.value?.validate((valid) => {
				if (valid) {
					switch (_data.dialogType) {
						case dialogTypeOpt.create:
							_methods.confirmCreate();
							break;
						case dialogTypeOpt.update:
							_methods.confirmUpdate();
							break;
						default:
							_methods.confirmHandle?.();
							break;
					}
				}
			});
		},
		confirmHandle() {},
		// 增
		handleCreate() {
			_methods.resetTemp();
			_data.dialogType = dialogTypeOpt.create;
			_data.dialogVisible = true;
		},
		// 确定创建
		async confirmCreate() {
			if (apiOpt?.create) {
				const query = _methods.getCreateQuery();
				_data.handleLoading = true;
				const response = await apiOpt.create(query).catch(() => (_data.handleLoading = false));
				_methods.handleSuccess(response);
			} else {
				console.log("未定义create接口或confirmCreate方法");
			}
		},
		getCreateQuery() {
			return _data.temp;
		},
		// 删
		handleDelete(data, cb) {
			_data.temp = data;
			_methods.showConfirm(cb || _methods.confirmDelete);
		},
		// 批量删除
		handleBatchDel(cb) {
			_methods.showConfirm(cb || _methods.confirmBatchDelete);
		},
		confirmBatchDelete() {
			console.log("未定义confirmBatchDelete方法");
		},
		// 确定删除
		async confirmDelete() {
			if (apiOpt?.del) {
				const query = _methods.getDelQuery();
				if (query === false) return;
				const response = await apiOpt.del(query);
				_methods.handleSuccess(response);
			} else {
				console.log("未定义del接口或confirmDelete方法");
			}
		},
		getDelQuery() {
			if (!_data.temp.id) {
				alert("找后端要列表id");
				return false;
			}
			return { id: _data.temp.id };
		},
		// 改之前摇
		async handleUpdateBefore(row, next) {
			const data = await _methods.getDetailData(row);
			if (data === false) return;
			_methods.handleUpdate(data, next);
		},
		async getDetailData(row) {
			if (apiOpt?.detail) {
				const query = _methods.getDetailQuery(row);
				if (query === false) return;
				const { ok, data } = await apiOpt.detail(query);
				if (ok) {
					return data;
				}
			} else {
				console.log("未定义detail接口或handleUpdateBefore方法");
			}
			return false;
		},
		getDetailQuery({ id }) {
			if (!id) {
				alert("找后端要列表id");
				return false;
			}
			return { [apiOpt.detailIdKey || "id"]: id };
		},
		// 改
		handleUpdate(row, next) {
			_methods.resetTemp();
			_data.temp = { ...row };
			if (typeCheck(next) === "[object Function]" || typeCheck(next) === "[object AsyncFunction]") {
				next(row);
			}
			_data.dialogType = dialogTypeOpt.update;
			_data.dialogVisible = true;
		},
		// 确定修改
		async confirmUpdate() {
			if (apiOpt?.update) {
				const query = _methods.getUpdateQuery();
				_data.handleLoading = true;
				const response = await apiOpt.update(query).catch(() => (_data.handleLoading = false));
				_methods.handleSuccess(response);
			} else {
				console.log("未定义update接口或confirmUpdate方法");
			}
		},
		getUpdateQuery() {
			return _data.temp;
		},
		// 增删改成功
		handleSuccess({ ok }) {
			_data.handleLoading = false;
			if (ok) {
				ElMessage.closeAll("success");
				ElMessage.success("操作成功");
				_data.dialogVisible = false;
				_methods.getTableData();
			}
		},
		//详情
		async handleDetails(row, next) {
			const data = await _methods.getDetailData(row);
			if (data === false) return;
			_methods.resetTemp();
			_data.temp = { ...data };
			if (typeCheck(next) === "[object Function]" || typeCheck(next) === "[object AsyncFunction]") {
				next(data);
			}
			_data.dialogType = dialogTypeOpt.detail;
			_data.dialogVisible = true;
		},
		getTableDataNormalSuccess({ ok, data, count }) {
			_data.tableLoading = false;
			if (ok) {
				_data.tableData = data;
				_data.total = count ?? data.length;
			} else {
				_data.tableData = [];
			}
		},
		// 页码
		handleCurrentChange(pageIndex) {
			_data.tableQuery.pageIndex = pageIndex;
			_methods.getTableData();
		},
		// 单页数据量
		handleSizeChange(pageSize) {
			_data.tableQuery.pageIndex = 1;
			_data.tableQuery.pageSize = pageSize;
			_methods.getTableData();
		},
		resetTemp() {
			_data.temp = {};
			_data.handleLoading = false;
			formRef.value?.resetFields();
		},
		reset() {
			_data.tableQuery = {
				pageIndex: 1,
				pageSize: 20
			};
			tableRef.value?.tableInstance?.clearSelection();
			_methods.getTableData();
		},
		async handleImport() {
			if (apiOpt?.import) {
				const file = await selectLocalFile(fileType.xlsx);
				const query = _methods.getImportQuery(file);
				_data.handleLoading = true;
				const loading = ElLoading.service({ lock: true, text: "正在导入,请稍后" });
				const response = await apiOpt.import(obj2Formdata(query));
				loading.close();
				_methods.handleImportSuccess(response);
			} else {
				console.log("未定义import接口或handleImport方法");
			}
		},
		// 上传接口调用成功
		handleImportSuccess(res) {
			_data.handleLoading = false;
			const type = typeCheck(res);
			switch (true) {
				case type === "[object Object]":
					_methods.handleImportSomeSuccess(res);
					break;
				case type === "[object Blob]" && res.type.includes("json"):
					{
						const reader = new FileReader();
						reader.readAsText(res, "utf-8");
						reader.onload = () => {
							_methods.handleImportSomeSuccess(JSON.parse(reader.result));
						};
					}
					break;
				case type === "[object Blob]":
					_methods.getTableData();
					ElMessageBox.alert("部分上传成功,请修改失败的部分重新上传", "系统提示", { autofocus: false, type: "warning" });
					downloadFile(res, "导入失败部分.xlsx");
					break;
				default:
					break;
			}
		},
		// 上传内容部分成功
		handleImportSomeSuccess(res) {
			const { ok, data } = res;
			if (ok) {
				const { errorCount } = data || {};
				if (!errorCount) {
					// 全部成功,没有失败
					ElMessage.closeAll("success");
					ElMessage.success("导入成功");
					_methods.handleSuccess(res);
				} else {
					// 部分成功,对失败部分处理成表格
					ElMessageBox.alert("部分上传成功,请修改失败的部分重新上传", "系统提示", { autofocus: false, type: "warning" });
					_methods.handleImportError(res);
				}
			}
		},
		// 上传内容部分失败处理
		handleImportError() {
			console.log("未定义handleImportError方法");
		},
		getImportQuery(file) {
			return { file };
		},
		async handleDownload() {
			if (apiOpt?.download) {
				const query = _methods.getDownloadQuery();
				_data.handleLoading = true;
				const response = await apiOpt.download(query);
				_data.handleLoading = false;
				downloadFile(response, `${_data.fileName || Date.now()}.xlsx`);
				return Promise.resolve();
			} else {
				console.log("未定义download接口或handleDownload方法");
			}
		},
		getDownloadQuery() {
			return null;
		},
		// 获取表格模板
		async handleTemplate() {
			console.log("未定义handleTemplate方法");
		},
		fetchData() {},
		...methods
	};

	const Table = (props, { slots }) => {
		return h(
			_Table,
			{
				ref: tableRef,
				data: _data.tableData,
				total: _data.total,
				tableQuery: _data.tableQuery,
				loading: _data.tableLoading,
				...props,
				onHandleSizeChange: _methods.handleSizeChange,
				onHandleCurrentChange: _methods.handleCurrentChange,
				onHandleBatchDel: _methods.handleBatchDel
			},
			slots
		);
	};

	return { Table, tableRef, formRef, ...toRefs(_data), ..._methods };
};