ztz-table
Version:
ZTZ Table,一个为开发者准备的基于 Vue 3.0 和 Element Plus 的数据表格二次封装的组件库,旨在通过配置文件快速生成表格。集成CRUD功能,通过简单配置,快速完成一个基本的增删查改功能。
618 lines (617 loc) • 27.3 kB
JSX
import { get, merge, debounce, cloneDeep, } from 'lodash';
import { h, ref, watch, computed, Fragment, watchEffect, nextTick, defineComponent, useAttrs, useSlots, getCurrentInstance, } from 'vue';
import { ElSpace, ElTable, ElDialog, ElButton, ElMessage, ElPopconfirm, ElPagination, ElTableColumn, } from 'element-plus';
import { getProp, getLabel, checkType, getObjectValue, sortCurdVNodeBtn, mergeDifference, } from '../utils/util';
import { TablePagination, TableDataRowOptions, } from './interface';
class CrudAdd {
api = undefined;
formComponent = undefined;
successMsg = '添加成功';
errorMsg = null;
dialogProps = {
width: '700px',
title: '添加',
'append-to-body': true,
};
}
class CrudEdit {
api = undefined;
detailApi = undefined;
formComponent = undefined;
successMsg = '修改成功';
errorMsg = undefined;
dialogProps = {
width: '700px',
title: '修改',
'append-to-body': true,
};
buttonProps = {
type: 'default',
size: 'small',
plain: true,
};
}
class CrudDelete {
api = undefined;
crudSort = 30;
confirmText = '确定要删除吗?';
successMsg = '删除成功';
errorMsg = null;
buttonProps = {
type: 'danger',
size: 'small',
plain: true,
};
}
export default defineComponent({
name: 'ztz-table',
props: {
columns: {
type: Array,
default: () => [],
},
data: {
type: [Array, Function],
default: () => [],
},
queryParams: {
type: Object,
default: () => ({}),
},
crud: {
type: Object,
default: () => ({}),
},
// 是否立即执行搜索
immediate: {
type: Boolean,
default: true,
},
// 是否根据queryParams搜索条件动态变化,执行刷新列表
dynamic: {
type: Boolean,
default: false,
},
// 分页属性
pagination: {
type: Object,
default: () => (new TablePagination()),
},
// 影藏分页
hidePagination: Boolean,
// 表格数据Key
listKey: {
type: String,
default: 'content',
},
// 总条数Key
totalKey: {
type: String,
default: 'total',
},
emptyText: {
type: String,
default: '暂无数据',
},
},
setup(props, { expose }) {
const attrs = useAttrs();
const slots = useSlots();
const ins = getCurrentInstance();
const tableDataRef = ref([]);
const addFormRef = ref(null);
const editFormRef = ref(null);
const visibleAddDialogRef = ref(false);
const visibleEditDialogRef = ref(false);
const currentCrudRef = ref({});
const columnVNodeRenderRef = ref(null);
const loadingAddRef = ref(false);
const loadingEditRef = ref(false);
const loadingEditEntityRef = ref(false);
const loadingTableDataRef = ref(false);
const paginationRef = ref(props.pagination);
const paginationBackupRef = ref(props.pagination);
const editFormModelRef = ref({});
const queryParamsRef = ref({});
const queryParamsBackupRef = ref(null);
const requestContentRef = ref({});
const hasAddComputed = computed(() => !!currentCrudRef.value?.add?.api && !!currentCrudRef.value?.add?.formComponent);
const hasEditComputed = computed(() => !!currentCrudRef.value?.edit?.api && !!currentCrudRef.value?.edit?.formComponent);
const hasDeleteComputed = computed(() => !!currentCrudRef.value?.delete?.api);
const hidePaginationComputed = computed(() => {
if (typeof props.hidePagination === 'boolean') {
return props.hidePagination;
}
return typeof props.data !== 'function';
});
// 表格数据赋值,一定要使用这个方法
const setTableData = (data = []) => {
data.forEach((p) => {
const tableDataRowOptions = new TableDataRowOptions();
if (p._options && checkType(p._options, 'Object')) {
p._options?.uid == null && merge(p._options, tableDataRowOptions);
}
else {
p._options = tableDataRowOptions;
}
});
tableDataRef.value = data;
};
// 创建CRUD功能按钮
const createCrudBtn = function (config, ...args) {
const { row } = args[0];
const result = [];
const options = row?._options;
const editPermission = options?.permission?.delete;
const deletePermission = options?.permission?.delete;
const getInjectStatus = (type) => {
let isInject = getObjectValue(config, type);
if (typeof isInject !== 'boolean') {
isInject = config.label === '操作';
}
if (typeof getObjectValue(options, 'injectEdit') === 'boolean') {
isInject = getObjectValue(options, 'injectEdit');
}
return isInject;
};
const injectEdit = getInjectStatus('injectEdit');
const injectDelete = getInjectStatus('injectDelete');
// 编辑功能按钮
if (hasEditComputed.value
&& injectEdit
&& editPermission !== false) {
result.push(h(ElButton, {
...currentCrudRef.value.edit.buttonProps,
curdSort: currentCrudRef.value.edit.curdSort,
disabled: options.disabledEdit || options.loadingDelete,
onClick: (e) => {
handleEdit.apply(null, [...args, e]);
},
}, ['编辑']));
}
// 删除功能按钮
if (hasDeleteComputed.value
&& injectDelete
&& deletePermission !== false) {
result.push(h(ElPopconfirm, {
cancelButtonText: '取消',
confirmButtonText: '确定',
title: currentCrudRef.value.delete.confirmText,
onConfirm: () => {
handleDelete.apply(null, [...args]);
},
}, {
reference: () => h(ElButton, {
...currentCrudRef.value.delete.buttonProps,
curdSort: currentCrudRef.value.delete.curdSort,
loading: options?.loadingDelete,
disabled: options.disabledDelete,
}, ['删除']),
}));
}
return result;
};
// 向表格中注入CRUD的功能
const injectCurd2ColumnVNode = (columnVNodeList = []) => {
columnVNodeList = columnVNodeList.map((columnVNode) => {
const renderHeader = columnVNode?.children?.renderHeader ?? columnVNode?.children?.header;
const renderDefault = columnVNode?.children?.render ?? columnVNode?.children?.default;
const label = getLabel(columnVNode.props);
const prop = getProp(columnVNode.props);
const config = {
label,
injectEdit: getObjectValue(columnVNode.props, 'injectEdit'),
injectDelete: getObjectValue(columnVNode.props, 'injectDelete'),
};
// 自定义渲染-头
if (typeof renderHeader === 'function') {
columnVNode.children.header = renderHeader;
}
// 自定义渲染-body
if (typeof renderDefault === 'function') {
columnVNode.children.default = function (...argsList) {
let renderVNodeList = [];
const renderVNode = renderDefault.apply(this, argsList);
if (renderVNode) {
if (Array.isArray(renderVNode)) {
renderVNodeList = renderVNode;
}
else {
renderVNodeList = [renderVNode];
}
}
createCrudBtn.apply(null, [config, ...argsList]).forEach((n) => {
renderVNodeList.push(n);
});
return sortCurdVNodeBtn(renderVNodeList);
};
}
else {
// 重新创建虚拟节点,更新 ShapeFlags
columnVNode = h(columnVNode, { key: String(Date.now()) }, {
// @ts-ignore
...(checkType(columnVNode.children, 'Object') ? columnVNode.children : {}),
...{
default(...argsList) {
const { row } = argsList[0];
const renderVNode = createCrudBtn.apply(this, [config, ...argsList]);
if (prop != null) {
renderVNode.push(h('span', {}, [getObjectValue(row, prop)]));
}
return renderVNode;
},
},
});
}
return columnVNode;
});
return columnVNodeList;
};
// 搜索数据处理
watchEffect(() => {
if (Object.keys(props.queryParams).length > 0 && queryParamsBackupRef.value == null) {
queryParamsBackupRef.value = cloneDeep(props.queryParams);
}
queryParamsRef.value = props.queryParams;
});
let queryParamsWatcher = null;
// 根据queryParams深度监听动态变化,进行搜索
watch(() => props.dynamic, (v) => {
if (v) {
if (queryParamsWatcher == null) {
queryParamsWatcher = watch(() => props.queryParams, () => {
refreshTable({
resetPageNum: true,
});
}, { deep: true });
}
}
else {
queryParamsWatcher?.();
}
}, { immediate: true });
// 表格数据
watchEffect(() => {
if (Array.isArray(props.data)) {
setTableData(props.data);
}
});
// 分页配置
watchEffect(() => {
if (Object.keys(props.pagination).length > 0 && paginationBackupRef.value == null) {
paginationBackupRef.value = cloneDeep(props.pagination);
}
paginationRef.value = merge(new TablePagination(), paginationBackupRef.value);
});
// 分页字段获取
watchEffect(() => {
if (typeof props.pagination?.pageSize === 'number') {
paginationRef.value.pageSize = props.pagination?.pageSize;
}
if (typeof props.pagination?.pageNum === 'number') {
paginationRef.value.pageNum = props.pagination?.pageNum;
}
// @ts-ignore 原字段
if (typeof props.pagination?.currentPage === 'number') {
// @ts-ignore 原字段
paginationRef.value.pageNum = props.pagination?.currentPage;
}
if (typeof props.pagination?.layout === 'string') {
paginationRef.value.layout = props.pagination?.layout;
}
});
// JSON配置渲染表格
watchEffect(() => {
if (typeof slots.columns !== 'function') {
const columnVNodeList = props.columns.map((column, index) => {
column = cloneDeep(column);
// 差异
const label = getLabel(column);
column.slots = {};
column.prop = getProp(column);
column.label = label;
column.key = column.key ?? index;
const children = {};
// 自定义渲染
const renderHeader = column.renderHeader ?? column.header;
const renderDefault = column.render ?? column.default;
if (typeof renderHeader === 'function') {
children.header = renderHeader;
}
if (typeof renderDefault === 'function') {
children.default = renderDefault;
}
return h(ElTableColumn, {
...column,
}, Object.keys(children).length > 0 ? children : undefined);
});
columnVNodeRenderRef.value = h(Fragment, null, injectCurd2ColumnVNode(columnVNodeList));
}
});
// CRUD配置合并
watchEffect(() => {
if (checkType(props.crud, 'Object')) {
props.crud?.add && mergeDifference(props.crud.add, new CrudAdd());
props.crud?.edit && mergeDifference(props.crud.edit, new CrudEdit());
props.crud?.delete && mergeDifference(props.crud.delete, new CrudDelete());
currentCrudRef.value = props.crud;
}
else {
currentCrudRef.value = {};
}
});
// template 语法渲染表格结构
if (typeof slots.columns === 'function') {
columnVNodeRenderRef.value = h(Fragment, null, injectCurd2ColumnVNode(slots?.columns.call(ins)));
}
// 新增,添加
const showAddDialog = () => {
if (hasAddComputed.value) {
visibleAddDialogRef.value = true;
}
};
// 编辑,修改
const handleEdit = (data) => {
let { row } = data;
visibleEditDialogRef.value = true;
if (row?._options?.uid === editFormModelRef.value?._options?.uid) {
return;
}
nextTick(() => {
const formRef = editFormRef.value?.getFormRef?.();
// 重置表单
formRef?.resetFields?.();
formRef?.clearValidate?.();
row = cloneDeep(row);
loadingEditRef.value = false;
editFormModelRef.value = row;
// 判断是否有详情接口,有详情接口使用详情接口获取实体
if (typeof currentCrudRef.value.edit.detailApi === 'function') {
const params = { ...row };
delete params._options;
loadingEditEntityRef.value = true;
currentCrudRef.value.edit.detailApi(params).then((res = {}) => {
res = checkType(res, 'Object') ? res : {};
if (editFormModelRef.value?._options?.uid === row?._options?.uid) {
res._options = row._options;
editFormModelRef.value = res;
loadingEditEntityRef.value = false;
}
});
}
else {
loadingEditEntityRef.value = false;
}
});
};
// 删除
const handleDelete = (data) => {
const params = cloneDeep(data.row);
delete params._options;
if (data.row._options) {
data.row._options.loadingDelete = true;
}
currentCrudRef.value?.delete?.api(params).then(() => {
const msg = currentCrudRef.value.delete.successMsg;
refreshTable();
if (msg) {
ElMessage({
showClose: true,
message: msg,
type: 'success',
});
}
}).catch(() => {
if (data.row._options) {
data.row._options.loadingDelete = false;
}
});
};
// 请求表格数据
const execFetchTableData = () => {
if (typeof props.data === 'function') {
const params = cloneDeep(Object.assign({}, queryParamsRef.value, {
pageNum: paginationRef.value.pageNum,
pageSize: paginationRef.value.pageSize,
}));
loadingTableDataRef.value = true;
const e = props.data(params);
if (!(e instanceof Promise)) {
throw new Error('列表接口必须返回Promise实例');
}
e.then((res) => {
setTableData(get(res, props.listKey));
paginationRef.value.total = get(res, props.totalKey);
requestContentRef.value = res;
}).finally(() => {
loadingTableDataRef.value = false;
});
}
else {
loadingTableDataRef.value = false;
}
};
const fetchTableData = debounce(execFetchTableData, 250, { maxWait: 700, leading: false, trailing: true });
/**
* @description 刷新表格数据
* @param options { Object= } 可选配置
* @param options.resetPageNum { boolean= } 重置分页到初始值
* @param options.resetQueryParams { boolean= } 重置搜索条件到初始值
*/
const refreshTable = (options = { resetPageNum: false, resetQueryParams: false }) => {
if (options?.resetPageNum) {
paginationRef.value.pageNum = paginationBackupRef.value.pageNum ?? 1;
}
if (options?.resetQueryParams && queryParamsBackupRef.value) {
for (const [k, v] of Object.entries(queryParamsBackupRef.value)) {
queryParamsRef.value[k] = v;
}
}
fetchTableData();
};
const handleSizeChange = (val) => {
paginationRef.value.pageSize = val;
refreshTable();
};
const handleCurrentChange = (val) => {
paginationRef.value.pageNum = val;
refreshTable();
};
// 修改和新增的时候,提交表单
const handleSubmit = (type) => {
const isAdd = type === 'add';
const formRef = isAdd ? addFormRef.value?.getFormRef?.() : editFormRef.value?.getFormRef?.();
const entity = cloneDeep(isAdd ? addFormRef.value?.getFormModel?.() : editFormRef.value?.getFormModel?.());
const c = isAdd ? currentCrudRef.value.add : currentCrudRef.value.edit;
const options = entity._options;
delete entity._options;
formRef?.validate((valid) => {
if (valid) {
if (isAdd) {
loadingAddRef.value = true;
}
else {
loadingEditRef.value = true;
}
c.api(entity).then((d) => {
if (!isAdd && options?.uid !== editFormModelRef.value?._options?.uid) {
return d;
}
const msg = c.successMsg;
formRef.resetFields();
formRef.clearValidate();
if (isAdd) {
visibleAddDialogRef.value = false;
}
else {
visibleEditDialogRef.value = false;
editFormModelRef.value = {};
}
refreshTable();
if (msg) {
ElMessage({
showClose: true,
message: msg,
type: 'success',
});
}
}).catch((e) => {
if (!isAdd && options?.uid !== editFormModelRef.value?._options?.uid) {
return e;
}
const msg = c.errorMsg;
if (msg) {
ElMessage({
showClose: true,
message: msg,
type: 'error',
});
}
}).finally(() => {
if (!isAdd && options?.uid !== editFormModelRef.value?._options?.uid) {
return;
}
if (isAdd) {
loadingAddRef.value = false;
}
else {
loadingEditRef.value = false;
}
});
}
});
};
// 立即执行搜索
if (props.immediate) {
refreshTable();
}
// 对外暴露的方法
expose({
showAddDialog,
refreshTable,
});
// @ts-ignore
return () => (<>
<div class="ztz-table-wrapper">
{/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */}
<div v-loading={loadingTableDataRef.value} class="ztz-table__body">
{/* 表格区域 */}
<div class="ztz-table__content">
{/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */}
<ElTable {...attrs} emptyText={props.emptyText} data={tableDataRef.value} style={{ width: '100%' }}>
{// @ts-ignore
h(columnVNodeRenderRef.value)}
</ElTable>
</div>
{/* 分页区域 */}
{!hidePaginationComputed.value &&
<div class={{
'ztz-table__pagination': true,
'ztz-table__pagination--right': paginationRef.value.position === 'right',
}}>
{/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */}
<ElSpace>
{slots.paginationLeftSide &&
<div>
{/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */}
<slot {...requestContentRef.value} name="paginationLeftSide"/>
</div>}
{/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */}
<ElPagination {...attrs} v-model:currentPage={paginationRef.value.pageNum} layout={paginationRef.value.layout} total={paginationRef.value.total} page-size={paginationRef.value.pageSize}
// @ts-ignore
onSizeChange={handleSizeChange} onCurrentChange={handleCurrentChange} ref="paginationRef"/>
{slots.paginationRightSide &&
<div>
{/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */}
<slot {...requestContentRef.value} name="paginationRightSide"/>
</div>}
</ElSpace>
</div>}
</div>
</div>
{/* 新增模态框 */hasAddComputed.value &&
/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */
<ElDialog {...currentCrudRef.value?.add?.dialogProps} v-model={visibleAddDialogRef.value}>
{{
default: () => (<div v-loading={loadingAddRef.value}>
{/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */}
{h(currentCrudRef.value.add.formComponent, {
ref: (r) => {
addFormRef.value = r;
},
})}
</div>),
footer: () => (<>
{/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */}
<ElButton onClick={() => visibleAddDialogRef.value = false}>取消</ElButton>
{/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */}
<ElButton loading={loadingAddRef.value} onClick={() => handleSubmit('add')} type="primary">确定</ElButton>
</>),
}}
</ElDialog>}
{/* 编辑模态框 */hasEditComputed.value &&
/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */
<ElDialog {...currentCrudRef.value.edit.dialogProps} v-model={visibleEditDialogRef.value}>
{{
default: () => (<div v-loading={loadingEditEntityRef.value || loadingEditRef.value}>
{/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */}
{h(currentCrudRef.value.edit.formComponent, {
data: editFormModelRef.value,
ref: (r) => {
editFormRef.value = r;
},
})}
</div>),
footer: () => (<>
{/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */}
<ElButton onClick={() => visibleEditDialogRef.value = false}>取消</ElButton>
{/* @ts-ignore 忽略 Type 'Element' is not assignable to type 'ReactNode'. 错误 */}
<ElButton loading={loadingEditRef.value} disabled={loadingEditEntityRef.value} onClick={() => handleSubmit('edit')} type="primary">
确定
</ElButton>
</>),
}}
</ElDialog>}
</>);
},
});