由于之前公司有个特殊,中台的代码有大部分是后端java来完成,这样就是考验组件是否更合理,el-table已经是优秀的组件,但是不免产品和UI有自己想法修改稍微调整,需要完成的功能
- 第一列靠左固定,最后一列(操作)靠右固定,有默认固定宽度
- table的单选,需要修改成el-radio
- 和后端接口请求在一起,支持分页
- 当表格数据大时候需要前端自己分页
- 支持拖拽排序(7-22补充)
第一列靠左固定,最后一列(操作)靠右固定
这个相比最简单,如果不封装组件<el-table-column prop="date" label="Date" fixed width="123" />就是设置fixed属性和width属性, 如果封装的话拦截slots.default对于子节点拦截处理,具体代码如下
实现代码
function useChildren(slots: any) {
const items = slots && slots.default ? slots.default() : [];
return items.map((tableColumn: VNode, tableColumnIndex: number) => {
let fixed: boolean | string = false;
tableColumnIndex === 0 && (fixed = "left");
tableColumnIndex === items.length - 1 && (fixed = "right");
return cloneVNode(tableColumn, {
fixed,
minWidth: tableColumn?.props?.minWidth || 128,
});
});
}
实现效果
table的单选,需要修改成el-radio
先看看自带的效果
说2点自己的看法
- 看不出来是单选
- 选择后的事件和多选不一致
怎么去解决这个问题呢?在表格最左边添加栏目内容是el-radio, 内部记录radioId,当点击响应切换
我们先定义类型
export type Key = number | string;
export interface Data {
id: Key;
[key: string]: any;
}
export interface Scope {
row: Data;
}
export interface RowSelection {
type?: "checkbox" | "radio";
selectedRowKeys?: Key[];
onChange?: (selectedRowKeys: Key[], selectedRows: Data[]) => void;
}
实现代码
function useRadio(rowSelection: RowSelection, dataSource: Ref<Data[]>) {
const radioId = ref(rowSelection.selectedRowKeys?.[0]);
const onChange = () => {
if (rowSelection.onChange) {
const checkedIds = ref(radioId.value ? [radioId.value] : []);
rowSelection.onChange(
toRaw(checkedIds.value),
dataSource.value.filter((x) => checkedIds.value.includes(x.id))
);
}
};
const handleRadioChange = (id: Key) => {
radioId.value = id;
onChange();
};
const node = rowSelection.type === "radio" && (
<el-table-column width="50" prop="id" fixed>
{(scope: Scope) => (
<el-radio
modelValue={radioId.value}
label={scope.row.id}
onChange={() => handleRadioChange(scope.row.id)}
>
{" "}
</el-radio>
)}
</el-table-column>
);
return {
node,
radioId,
handleRadioChange,
};
}
实现效果
table的多选,都是同一属性RowSelection
刚才反馈的单选和多选操作属性太多,api太多,既然封装了,我们就把2个整合成一个
查看源码
function useCheckbox(rowSelection: RowSelection, dataSource: Ref<Data[]>) {
const checkedIds = ref<Key[]>(rowSelection.selectedRowKeys || []);
const isAllChecked = computed(() => {
return dataSource.value.every((row) => checkedIds.value.includes(row.id));
});
const hasChecked = computed(() => {
return dataSource.value.some((row) => checkedIds.value.includes(row.id));
});
const onChange = () => {
if (rowSelection.onChange) {
rowSelection.onChange(
toRaw(checkedIds.value),
dataSource.value.filter((x) => checkedIds.value.includes(x.id))
);
}
};
const handleCheckboxChange = (id: Key) => {
const index = checkedIds.value.indexOf(id);
if (index >= 0) {
checkedIds.value.splice(index, 1);
} else {
checkedIds.value.push(id);
}
onChange();
};
const handleAllCheckboxChange = () => {
const ids = dataSource.value.map((x) => x.id);
if (isAllChecked.value) {
checkedIds.value = checkedIds.value.filter((id) => !ids.includes(id));
} else {
// 当前页,不在checkedIds,去选中
dataSource.value
.filter((row) => !checkedIds.value.includes(row.id))
.forEach((row) => {
checkedIds.value.push(row.id);
});
}
onChange();
};
const node = rowSelection.type === "checkbox" && (
<el-table-column width="50" prop="id" fixed>
{{
header: () => (
<el-checkbox
model-value={isAllChecked.value}
indeterminate={isAllChecked.value ? false : hasChecked.value}
onChange={handleAllCheckboxChange}
/>
),
default: (scope: Scope) => (
<el-checkbox
model-value={checkedIds.value.includes(scope.row.id)}
onChange={() => handleCheckboxChange(scope.row.id)}
/>
),
}}
</el-table-column>
);
return {
node,
checkedIds,
isAllChecked,
hasChecked,
handleCheckboxChange,
handleAllCheckboxChange,
};
}
查看效果
和后端接口请求在一起,支持分页
大部分我们的表格都是和分页结合在一起,el-pagination的属性基本上已经满足我们日常需求,我们之需要把2者整合在一起,同样的为了简单原则,我们先定义
export interface Pagination {
total?: number; // 后端分页,只需要传递这个
pageSize?: number; // 每页展示数量,默认10
virtual?: boolean; // 前端分页,设置成true
onSizeChange?: (pageSize: number) => void;
onCurrentChange?: (currentPage: number) => void;
}
具体代码如下
function usePagination(pagination: Pagination, data: Data[]) {
const basePageSize = pagination.pageSize || 10;
const pageSizes = [
basePageSize,
basePageSize * 2,
basePageSize * 3,
basePageSize * 5,
];
const currentPage = ref(1);
const pageSize = ref(basePageSize);
const handleSizeChange = (ps: number) => {
currentPage.value = 1;
pageSize.value = ps;
pagination?.onSizeChange && pagination?.onSizeChange(ps);
};
const handleCurrentChange = (cp: number) => {
currentPage.value = cp;
pagination?.onCurrentChange && pagination?.onCurrentChange(cp);
};
const showPagination =
pagination.virtual || (pagination?.total as number) > 0;
const node = showPagination && (
<el-pagination
pageSize={basePageSize}
pageSizes={pageSizes}
total={pagination.virtual ? data.length : pagination?.total}
background
layout="total, sizes, prev, pager, next, jumper"
onSizeChange={(val: number) => handleSizeChange(val)}
onCurrentChange={(val: number) => handleCurrentChange(val)}
style="display: flex;justify-content: flex-end;margin-top: 12px;"
/>
);
const dataSource = computed(() => {
if (!pagination.virtual) {
return data;
}
const val = (currentPage.value - 1) * pageSize.value;
return data.slice(val, val + pageSize.value);
});
return {
node,
dataSource,
pageSizes,
handleSizeChange,
};
}
查看效果
整体代码
import {
defineComponent,
cloneVNode,
ref,
toRaw,
computed,
PropType,
VNode,
Ref,
} from "vue";
import "./index.css";
export type Key = number | string;
export interface Data {
id: Key;
[key: string]: any;
}
export interface Scope {
row: Data;
}
export interface RowSelection {
type?: "checkbox" | "radio";
selectedRowKeys?: Key[];
onChange?: (selectedRowKeys: Key[], selectedRows: Data[]) => void;
}
export interface Pagination {
total?: number;
pageSize?: number;
virtual?: boolean;
onSizeChange?: (pageSize: number) => void;
onCurrentChange?: (currentPage: number) => void;
}
function useChildren(slots: any) {
const items = slots && slots.default ? slots.default() : [];
return items.map((tableColumn: VNode, tableColumnIndex: number) => {
let fixed: boolean | string = false;
tableColumnIndex === 0 && (fixed = "left");
tableColumnIndex === items.length - 1 && (fixed = "right");
return cloneVNode(tableColumn, {
fixed,
minWidth: tableColumn?.props?.minWidth || 128,
});
});
}
function useCheckbox(rowSelection: RowSelection, dataSource: Ref<Data[]>) {
const checkedIds = ref<Key[]>(rowSelection.selectedRowKeys || []);
const isAllChecked = computed(() => {
return dataSource.value.every((row) => checkedIds.value.includes(row.id));
});
const hasChecked = computed(() => {
return dataSource.value.some((row) => checkedIds.value.includes(row.id));
});
const onChange = () => {
if (rowSelection.onChange) {
rowSelection.onChange(
toRaw(checkedIds.value),
dataSource.value.filter((x) => checkedIds.value.includes(x.id))
);
}
};
const handleCheckboxChange = (id: Key) => {
const index = checkedIds.value.indexOf(id);
if (index >= 0) {
checkedIds.value.splice(index, 1);
} else {
checkedIds.value.push(id);
}
onChange();
};
const handleAllCheckboxChange = () => {
const ids = dataSource.value.map((x) => x.id);
if (isAllChecked.value) {
checkedIds.value = checkedIds.value.filter((id) => !ids.includes(id));
} else {
// 当前页,不在checkedIds,去选中
dataSource.value
.filter((row) => !checkedIds.value.includes(row.id))
.forEach((row) => {
checkedIds.value.push(row.id);
});
}
onChange();
};
const node = rowSelection.type === "checkbox" && (
<el-table-column width="50" prop="id" fixed>
{{
header: () => (
<el-checkbox
model-value={isAllChecked.value}
indeterminate={isAllChecked.value ? false : hasChecked.value}
onChange={handleAllCheckboxChange}
/>
),
default: (scope: Scope) => (
<el-checkbox
model-value={checkedIds.value.includes(scope.row.id)}
onChange={() => handleCheckboxChange(scope.row.id)}
/>
),
}}
</el-table-column>
);
return {
node,
checkedIds,
isAllChecked,
hasChecked,
handleCheckboxChange,
handleAllCheckboxChange,
};
}
function useRadio(rowSelection: RowSelection, dataSource: Ref<Data[]>) {
const radioId = ref(rowSelection.selectedRowKeys?.[0]);
const onChange = () => {
if (rowSelection.onChange) {
const checkedIds = ref(radioId.value ? [radioId.value] : []);
rowSelection.onChange(
toRaw(checkedIds.value),
dataSource.value.filter((x) => checkedIds.value.includes(x.id))
);
}
};
const handleRadioChange = (id: Key) => {
radioId.value = id;
onChange();
};
const node = rowSelection.type === "radio" && (
<el-table-column width="50" prop="id" fixed>
{(scope: Scope) => (
<el-radio
modelValue={radioId.value}
label={scope.row.id}
onChange={() => handleRadioChange(scope.row.id)}
>
{" "}
</el-radio>
)}
</el-table-column>
);
return {
node,
radioId,
handleRadioChange,
};
}
function usePagination(pagination: Pagination, data: Data[]) {
const basePageSize = pagination.pageSize || 10;
const pageSizes = [
basePageSize,
basePageSize * 2,
basePageSize * 3,
basePageSize * 5,
];
const currentPage = ref(1);
const pageSize = ref(basePageSize);
const handleSizeChange = (ps: number) => {
currentPage.value = 1;
pageSize.value = ps;
pagination?.onSizeChange && pagination?.onSizeChange(ps);
};
const handleCurrentChange = (cp: number) => {
currentPage.value = cp;
pagination?.onCurrentChange && pagination?.onCurrentChange(cp);
};
const showPagination =
pagination.virtual || (pagination?.total as number) > 0;
const node = showPagination && (
<el-pagination
pageSize={basePageSize}
pageSizes={pageSizes}
total={pagination.virtual ? data.length : pagination?.total}
background
layout="total, sizes, prev, pager, next, jumper"
onSizeChange={(val: number) => handleSizeChange(val)}
onCurrentChange={(val: number) => handleCurrentChange(val)}
style="display: flex;justify-content: flex-end;margin-top: 12px;"
/>
);
const dataSource = computed(() => {
if (!pagination.virtual) {
return data;
}
const val = (currentPage.value - 1) * pageSize.value;
return data.slice(val, val + pageSize.value);
});
return {
node,
dataSource,
pageSizes,
handleSizeChange,
};
}
export default defineComponent({
props: {
data: {
type: Array as PropType<Data[]>,
default: () => [],
},
rowSelection: {
type: Object as PropType<RowSelection>,
default: () => ({
selectedRowKeys: [],
}),
},
pagination: {
type: Object as PropType<Pagination>,
default: () => ({
pageSize: 10,
virtual: false,
}),
},
},
setup(props, { slots }) {
const children = useChildren(slots);
const { node: paginationNode, dataSource } = usePagination(
props.pagination,
props.data
);
const { node: checkboxNode } = useCheckbox(props.rowSelection, dataSource);
const { node: radioNode } = useRadio(props.rowSelection, dataSource);
return () => (
<>
<el-table data={dataSource.value}>
{checkboxNode}
{radioNode}
{children}
</el-table>
{paginationNode}
</>
);
},
});
调用代码
<script lang="ts" setup>
import ElTable2 from "./components/el-table2";
import type { Key } from "./components/el-table2";
const tableData = [
{
id: 1,
date: "2016-05-03",
name: "Tom",
state: "California",
city: "Los Angeles",
address: "No. 189, Grove St, Los Angeles",
zip: "CA 90036",
tag: "Home",
status: 0,
},
{
id: 2,
date: "2016-05-02",
name: "Tom",
state: "California",
city: "Los Angeles",
address: "No. 189, Grove St, Los Angeles",
zip: "CA 90036",
tag: "Office",
status: 1,
},
{
id: 3,
date: "2016-05-04",
name: "Tom",
state: "California",
city: "Los Angeles",
address: "No. 189, Grove St, Los Angeles",
zip: "CA 90036",
tag: "Home",
status: 2,
},
{
id: 4,
date: "2016-05-01",
name: "Tom",
state: "California",
city: "Los Angeles",
address: "No. 189, Grove St, Los Angeles",
zip: "CA 90036",
tag: "Office",
status: 0,
},
];
const handleClick = () => {
console.log("click");
};
const handleRowSelect = (selectedRowKeys: Key[]) => {
console.log("selectedRowKeys", selectedRowKeys);
};
const handlePaginationSizeChange = (val: number) => {
console.log("pageSize", val);
};
const handlePaginationCurrentChange = (val: number) => {
console.log("currentPage", val);
};
</script>
<template>
<h2>普通表格(左边、右边固定、固定最小宽度)</h2>
<el-table2 :data="tableData">
<el-table-column prop="date" label="Date" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="state" label="State" />
<el-table-column prop="city" label="City" />
<el-table-column prop="address" label="Address" width="600" />
<el-table-column prop="zip" label="Zip" />
<el-table-column label="Operations">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleClick">
编辑
</el-button>
<el-button v-if="row.status === 2" link type="primary" size="small">
删除
</el-button>
</template>
</el-table-column>
</el-table2>
<h2>多选</h2>
<el-table2
:data="tableData"
:row-selection="{
type: 'checkbox',
selectedRowKeys: [1, 2],
onChange: handleRowSelect,
}"
>
<el-table-column prop="date" label="Date" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="state" label="State" />
<el-table-column prop="city" label="City" />
<el-table-column prop="address" label="Address" width="600" />
<el-table-column prop="zip" label="Zip" />
<el-table-column label="Operations">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleClick">
编辑
</el-button>
<el-button v-if="row.status === 2" link type="primary" size="small">
删除
</el-button>
</template>
</el-table-column>
</el-table2>
<h2>单选</h2>
<el-table2
:data="tableData"
:row-selection="{
type: 'radio',
selectedRowKeys: [],
onChange: handleRowSelect,
}"
>
<el-table-column prop="date" label="Date" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="state" label="State" />
<el-table-column prop="city" label="City" />
<el-table-column prop="address" label="Address" width="600" />
<el-table-column prop="zip" label="Zip" />
<el-table-column label="Operations">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleClick">
编辑
</el-button>
<el-button v-if="row.status === 2" link type="primary" size="small">
删除
</el-button>
</template>
</el-table-column>
</el-table2>
<h2>带分页</h2>
<el-table2
:data="tableData"
:pagination="{
total: 100,
onSizeChange: handlePaginationSizeChange,
onCurrentChange: handlePaginationCurrentChange,
}"
>
<el-table-column prop="date" label="Date" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="state" label="State" />
<el-table-column prop="city" label="City" />
<el-table-column prop="address" label="Address" width="600" />
<el-table-column prop="zip" label="Zip" />
<el-table-column label="Operations">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleClick">
编辑
</el-button>
<el-button v-if="row.status === 2" link type="primary" size="small">
删除
</el-button>
</template>
</el-table-column>
</el-table2>
<h2>前端分页</h2>
<el-table2
:data="tableData"
:pagination="{
virtual: true,
pageSize: 2,
onSizeChange: handlePaginationSizeChange,
onCurrentChange: handlePaginationCurrentChange,
}"
>
<el-table-column prop="date" label="Date" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="state" label="State" />
<el-table-column prop="city" label="City" />
<el-table-column prop="address" label="Address" width="600" />
<el-table-column prop="zip" label="Zip" />
<el-table-column label="Operations">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleClick">
编辑
</el-button>
<el-button v-if="row.status === 2" link type="primary" size="small">
删除
</el-button>
</template>
</el-table-column>
</el-table2>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
</style>
拖拽排序
日常表格作为中台列表最重要的组件,也难免少不了拖拽排序功能,这里我们使用sortablejs组件
查看代码
export interface Drag {
oldIndex: number;
newIndex: number;
dataSource: Data[];
}
export interface Sortable {
drag?: boolean;
onDrag?: (drag: Drag) => void;
}
function useSortable(
sortable: Sortable,
elTable: Ref,
dataSource: Ref<Data[]>
) {
const drag = sortable.drag;
const onDrag = sortable.onDrag;
drag &&
onMounted(() => {
const tbody = elTable?.value?.querySelector(
".el-table__body-wrapper tbody"
);
const sortable =
tbody &&
Sortablejs.create(tbody, {
handle: ".sortable-handle", // Restricts sort start click/touch to the specified element
ghostClass: "sortable-ghost", // Class name for the drop placeholder,
setData: function (dataTransfer: any) {
// to avoid Firefox bug
// Detail see : https://github.com/RubaXa/Sortable/issues/1012
dataTransfer.setData("Text", "");
},
onEnd: (evt: any) => {
const targetRow = dataSource.value.splice(evt.oldIndex, 1)[0];
dataSource.value.splice(evt.newIndex, 0, targetRow);
onDrag &&
onDrag({
oldIndex: evt.oldIndex,
newIndex: evt.newIndex,
dataSource: dataSource.value,
});
// for show the changes, you can delete in you code
// const tempIndex = this.newList.splice(evt.oldIndex, 1)[0];
// this.newList.splice(evt.newIndex, 0, tempIndex);
},
});
onUnmounted(() => {
sortable.destroy();
});
});
const node = drag && (
<el-table-column width="30" fixed>
<el-icon class="sortable-handle">
<Menu />
</el-icon>
</el-table-column>
);
return { node };
}
查看效果
洋洋洒洒写了快2W字了,之前也封装过挺多table组件,要么通过传递大json,
- 各种属性传递,后来代码越来越大,导致维护成本越高
- 对于新手来说,本来记录el-table属性都挺多,还需要了解json的属性配置
但是通过组合式api封装不一样,可以把单独逻辑剥离成一个hooks,内部实现对应的逻辑,每个逻辑相互不依赖,也可以正常使用响应式数据、生命周期