最终用法
<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"
};
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 };
};