基于element-plus封装table组件
当前组件已经发布到npm库中,使用 pnpm add @dripadmin/drip-table 即可安装
表格本身是透明的, 实际使用时,需要自己设置背景色.
文章目录
- 1. 需求分析
- 2. 项目初始化与架构设计
- 2.1 项目初始化
- 2.2 目录结构设计
- 2.3 基础配置文件
- package.json
- tsconfig.json
- vite.lib.config.ts
- 3. 组件设计与实现
- 3.1 类型定义
- 3.2 组件实现
- 主组件实现
- 行操作工具栏组件
- 3.3 入口文件
- 4. 组件功能与API文档
- 4.1 DripTable 组件
- 属性
- 事件
- 插槽
- 4.2 行操作工具栏配置
- 5. 打包与发布
- 5.1 打包组件库
- 5.2 发布到NPM
- 5.2.1 准备发布
- 5.2.2 登录NPM
- 5.2.3 发布包
- 5.2.4 版本更新
- 5.3 使用发布的组件
- 6. 总结与进阶
1. 需求分析
Element Plus提供了功能强大的el-table
组件,但在实际业务系统中,存在大量重复的增删改查的操作,
所以针对现有的el-table进行封装后,只需要提供json类数据, 即可实现表单表格的操作, 提高我们的效率,
所需求大致如下:
- 简化配置:通过JSON配置生成复杂表格
- 工具栏:内置表格工具栏,支持自定义按钮和操作
- 行操作:支持行级别的操作按钮
- 分页控制:集成分页功能
- 自定义渲染:支持自定义列渲染
- 国际化:支持多语言
- 主题定制:支持自定义主题
- TypeScript支持:完整的类型定义
整体实现的效果如下图:
2. 项目初始化与架构设计
2.1 项目初始化
首先,我们需要创建一个基于Vue3和TypeScript的组件库项目:
# 创建项目目录
mkdir drip-table
cd drip-table# 初始化package.json
pnpm init# 安装核心依赖
pnpm add vue element-plus -P
pnpm add typescript vite @vitejs/plugin-vue @types/node -D
2.2 目录结构设计
组件库的目录结构如下:
drip-table/
├── packages/ # 组件源码
│ ├── components/ # 组件实现
│ │ ├── drip-table/ # 表格组件
│ │ │ ├── index.vue # 主组件
│ │ │ ├── toolbar/ # 工具栏组件
│ │ │ └── row-toolbar/ # 行操作组件
│ │ └── drip-form/ # 表单组件(可选)
│ ├── types/ # 类型定义
│ └── index.ts # 入口文件
├── playgrounds/ # 示例项目
│ └── drip-table-demo/ # 演示项目
├── package.json # 包配置
├── tsconfig.json # TypeScript配置
└── vite.lib.config.ts # 打包配置
2.3 基础配置文件
package.json
{"name": "drip-table","version": "0.1.0","description": "基于Element Plus的表格组件封装","main": "dist/drip-table.umd.js","module": "dist/drip-table.es.js","types": "dist/types/index.d.ts","files": ["dist"],"scripts": {"dev": "cd playgrounds/drip-table-demo && pnpm run dev","build": "vite build --config vite.lib.config.ts","preview": "cd playgrounds/drip-table-demo && pnpm run preview"},"keywords": ["vue3","element-plus","table","component"],"author": "drip admin","license": "MIT","peerDependencies": {"vue": "^3.2.0","element-plus": "^2.2.0"}
}
tsconfig.json
{"compilerOptions": {"target": "ES2020","useDefineForClassFields": true,"module": "ESNext","lib": ["ES2020", "DOM", "DOM.Iterable"],"skipLibCheck": true,"moduleResolution": "bundler","allowImportingTsExtensions": true,"resolveJsonModule": true,"isolatedModules": true,"noEmit": true,"jsx": "preserve","strict": true,"noUnusedLocals": true,"noUnusedParameters": true,"noFallthroughCasesInSwitch": true,"paths": {"@/*": ["./packages/*"]}},"include": ["packages/**/*.ts", "packages/**/*.d.ts", "packages/**/*.tsx", "packages/**/*.vue"],"references": [{ "path": "./tsconfig.node.json" }]
}
vite.lib.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';export default defineConfig({plugins: [vue()],build: {lib: {entry: resolve(__dirname, 'packages/index.ts'),name: 'DripTable',fileName: (format) => `drip-table.${format}.js`},rollupOptions: {external: ['vue', 'element-plus'],output: {globals: {vue: 'Vue','element-plus': 'ElementPlus'}}}},resolve: {alias: {'@': resolve(__dirname, 'packages')}}
});
3. 组件设计与实现
3.1 类型定义
首先,我们需要定义组件的类型:
// packages/types/drip-table.ts
import type { CSSProperties } from "vue";export type Align = "left" | "center" | "right";export interface RowToolbarAction {label: string;type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'default';disabled?: boolean;event: string;link?: boolean;
}export interface DripTableRowToolBar {label?: string;width?: number | string;align?: Align;fixed?: boolean | "left" | "right";size?: "small" | "default" | "large";actions: RowToolbarAction[];
}export interface DripTableColumn {label: string;prop?: string;type?: "selection" | "index" | "expand";width?: number | string;minWidth?: number | string;fixed?: boolean | "left" | "right";sortable?: boolean | "custom";align?: Align;headerAlign?: Align;showOverflowTooltip?: boolean;slot?: string;headerSlot?: string;children?: DripTableColumn[];
}export interface DripTablePagination {total: number;pageSize: number;currentPage: number;layout?: string;pageSizes?: number[];size?: "small" | "default" | "large";background?: boolean;align?: Align;
}export interface DripTableToolbarConfig {// 工具栏配置...
}export interface DripTableProps {data: any[];columns: DripTableColumn[];rowKey?: string;pagination?: DripTablePagination;toolbarLeft?: DripTableToolbarConfig;toolbarRight?: DripTableToolbarConfig;rowToolbar?: DripTableRowToolBar;height?: string | number;maxHeight?: string | number;stripe?: boolean;border?: boolean;size?: "large" | "default" | "small";fit?: boolean;showHeader?: boolean;highlightCurrentRow?: boolean;showOverflowTooltip?: boolean;emptyText?: string;defaultExpandAll?: boolean;expandRowKeys?: any[];defaultSort?: { prop: string; order: "ascending" | "descending" };tooltipEffect?: "dark" | "light";showSummary?: boolean;sumText?: string;summaryMethod?: (data: any) => any[];spanMethod?: (data: any) => any;selectOnIndeterminate?: boolean;indent?: number;lazy?: boolean;load?: (row: any, treeNode: any, resolve: (data: any[]) => void) => void;treeProps?: { children: string; hasChildren: string };tableLayout?: "fixed" | "auto";scrollbarAlwaysOn?: boolean;flexible?: boolean;
}export interface DripTableInstallOptions {locale?: string;i18n?: any;ssr?: boolean;
}
3.2 组件实现
主组件实现
<!-- packages/components/drip-table/index.vue -->
<template><ElConfigProvider :locale="elementLocale"><divclass="drip-table-wrapper":style="mergedWrapperStyle":id="String(tableKey)":class="[wrapperClass, { 'is-maximized': isMaximized, 'hide-ui': hideUIOnMaximize }]"><!-- 工具栏 --><div v-if="showAnyToolbar" class="drip-table__toolbars"><div class="drip-table__toolbar--left"><Toolbarv-if="toolbarLeftCfg":config="toolbarLeftCfg":columns="columns":data="data":table-key="tableKey"@refresh="emit('refresh')"@size-change="onSizeChange"@columns-visibility-change="onColumnsVisibilityChange"@columns-order-change="onColumnsOrderChange"@primary-action="emit('primary-action')"@maximize-toggle="onToggleMaximize"/> </div><div class="drip-table__toolbar--right"><Toolbarv-if="toolbarRightCfg":config="toolbarRightCfg":columns="columns":data="data":table-key="tableKey"@refresh="emit('refresh')"@size-change="onSizeChange"@columns-visibility-change="onColumnsVisibilityChange"@columns-order-change="onColumnsOrderChange"@primary-action="emit('primary-action')"@maximize-toggle="onToggleMaximize"/></div></div><!-- 表格 --><ElTableref="tableRef"v-bind="tableProps":data="data":height="tableHeight":max-height="tableMaxHeight":size="tableSize"@selection-change="emit('selection-change', $event)"@sort-change="emit('sort-change', $event)"@cell-click="emit('cell-click', $event)"@row-click="emit('row-click', $event)"><template v-for="(column, index) in visibleColumns" :key="index"><ElTableColumnv-if="!column.children || column.children.length === 0":prop="column.prop":label="column.label":type="column.type":width="column.width":min-width="column.minWidth":fixed="column.fixed":sortable="column.sortable":align="column.align":header-align="column.headerAlign":show-overflow-tooltip="column.showOverflowTooltip ?? showOverflowTooltip"><template #header="headerScope"><slot v-if="column.headerSlot" :name="column.headerSlot" :column="column" :scope="headerScope" /><span v-else>{{ column.label }}</span></template><template #default="scope"><slot v-if="column.slot" :name="column.slot" :row="scope.row" :column="column" :scope="scope" /><span v-else>{{ column.prop ? scope.row[column.prop] : '' }}</span></template></ElTableColumn><ElTableColumn v-else :label="column.label"><!-- 嵌套列 --><template v-for="(child, childIndex) in column.children" :key="`${index}-${childIndex}`"><!-- 嵌套列实现 --></template></ElTableColumn></template><!-- 行操作工具栏 --><ElTableColumn v-if="rowToolbar" :label="rowToolbar.label || '操作'" :width="rowToolbar.width || 100":align="rowToolbar.align || 'center'":fixed="rowToolbar.fixed || 'right'"><template #default="scope"><RowToolbar :actions="rowToolbar.actions || []" :row="scope.row":size="rowToolbar.size || 'small'"@action="(eventName, row) => emit('row-action', eventName, row)"/></template></ElTableColumn></ElTable><!-- 分页 --><div v-if="hasPagination" class="drip-table__pagination" :class="paginationClass" :style="paginationMerged?.style"><ElPaginationv-model:current-page="paginationState.currentPage"v-model:page-size="paginationState.pageSize":page-sizes="paginationMerged?.pageSizes":layout="paginationMerged?.layout":total="paginationMerged?.total ?? 0":background="paginationMerged?.background":size="paginationMerged?.size"@size-change="onPageSizeChange"@current-change="onCurrentPageChange"/></div></div></ElConfigProvider>
</template><script setup lang="ts">
import { ref, computed, watch, onMounted, nextTick } from 'vue';
import { ElTable, ElTableColumn, ElPagination, ElConfigProvider } from 'element-plus';
import type { DripTableProps, DripTableColumn, DripTablePagination } from '@/types/drip-table';
import Toolbar from './toolbar/index.vue';
import RowToolbar from './row-toolbar/index.vue';// 组件逻辑实现...
</script><style scoped>
.drip-table-wrapper {width: 100%;position: relative;
}.drip-table__toolbars {display: flex;justify-content: space-between;margin-bottom: 16px;
}.drip-table__pagination {margin-top: 16px;display: flex;justify-content: flex-end;
}/* 更多样式... */
</style>
行操作工具栏组件
<!-- packages/components/drip-table/row-toolbar/index.vue -->
<template><div class="drip-table-row-toolbar"><el-button-group v-if="group"><el-buttonv-for="action in props.actions":key="action.event":type="action.type":size="props.size || 'small'":link="action.link || false"@click="handleAction(action.event)">{{ action.label }}</el-button></el-button-group><template v-else><el-buttonv-for="action in props.actions":key="action.event":type="action.type":size="props.size || 'small'":link="action.link || false"@click="handleAction(action.event)">{{ action.label }}</el-button></template></div>
</template><script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
import type { RowToolbarAction } from '@/types/drip-table';const props = defineProps({actions: {type: Array as () => RowToolbarAction[],default: () => [],},row: {type: Object,default: () => ({}),},size: {type: String,default: 'small',},group: {type: Boolean,default: false,},
});const emit = defineEmits(['action']);const handleAction = (eventName: string) => {emit("action", eventName, props.row);
};
</script><style scoped>
.drip-table-row-toolbar {display: flex;gap: 8px;
}
</style>
3.3 入口文件
// packages/index.ts
import type { App } from 'vue';
import DripTableVue from './components/drip-table/index.vue';
import DripFormVue from './components/drip-form/index.vue';
import RowToolbarVue from './components/drip-table/row-toolbar/index.vue';
import type {DripTableProps,DripTableColumn,DripTablePagination,DripTableToolbarConfig,DripTableRowToolBar,DripTableInstallOptions,
} from './types/drip-table';
import type { DripFormConfig, DripFormItem } from './types/drip-form';export const DripTable = Object.assign(DripTableVue, {install(app: App, options?: DripTableInstallOptions) {app.component((DripTableVue as any).name || 'DripTable', DripTableVue);app.component('DripTableRowToolbar', RowToolbarVue);app.provide('locale', options ?? { locale: null, i18n: null, ssr: false });},
});export const DripForm = Object.assign(DripFormVue, {install(app: App) {app.component((DripFormVue as any).name || 'DripForm', DripFormVue);},
});export const DripTableRowToolbar = RowToolbarVue;export type {DripTableProps,DripTableColumn,DripTablePagination,DripTableToolbarConfig,DripTableRowToolBar,
};export type { DripFormConfig, DripFormItem };
4. 组件功能与API文档
4.1 DripTable 组件
属性
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
data | Array | [] | 表格数据 |
columns | Array | [] | 表格列配置 |
rowKey | String | ‘id’ | 行数据的唯一标识 |
pagination | Object | - | 分页配置 |
toolbarLeft | Object | - | 左侧工具栏配置 |
toolbarRight | Object | - | 右侧工具栏配置 |
rowToolbar | Object | - | 行操作工具栏配置 |
height | String/Number | - | 表格高度 |
maxHeight | String/Number | - | 表格最大高度 |
stripe | Boolean | false | 是否为斑马纹表格 |
border | Boolean | false | 是否带有边框 |
size | String | ‘default’ | 表格大小 |
… | … | … | 更多属性参考Element Plus的Table组件 |
事件
事件名 | 说明 | 参数 |
---|---|---|
selection-change | 当选择项发生变化时触发 | selection |
sort-change | 当表格的排序条件发生变化时触发 | { column, prop, order } |
row-click | 当某一行被点击时触发 | row, column, event |
row-action | 当行操作按钮被点击时触发 | eventName, row |
refresh | 当刷新按钮被点击时触发 | - |
page-change | 当页码改变时触发 | currentPage |
page-size-change | 当每页显示条数改变时触发 | pageSize |
primary-action | 当主操作按钮被点击时触发 | - |
插槽
插槽名 | 说明 | 作用域参数 |
---|---|---|
[column.slot] | 自定义列内容 | { row, column, scope } |
[column.headerSlot] | 自定义列头内容 | { column, scope } |
4.2 行操作工具栏配置
// 行操作工具栏配置示例
const rowToolbar = {label: '操作',width: 220,align: 'center',fixed: 'right',size: 'small',actions: [{ label: '新增', type: 'primary', event: 'add' },{ label: '修改', type: 'warning', event: 'edit' },{ label: '删除', type: 'danger', event: 'delete' }]
};
5. 打包与发布
5.1 打包组件库
# 执行打包命令
pnpm run build
打包后的文件将输出到dist
目录,包含以下文件:
drip-table.es.js
- ES模块格式drip-table.umd.js
- UMD格式types/
- TypeScript类型定义
5.2 发布到NPM
5.2.1 准备发布
- 确保
package.json
中的信息正确:
{
{"name": "@dripadmin/drip-table","version": "0.2.5","description": "","license": "MIT","type": "module","main": "dist/index.cjs","module": "dist/index.mjs","types": "dist/index.d.ts","exports": {".": {"types": "./dist/index.d.ts","import": "./dist/index.mjs","require": "./dist/index.cjs"},"./style.css": "./dist/style.css"},"files": ["dist","readme.md"],// ...其他配置
}
- 创建
.npmignore
文件,排除不需要发布的文件:
# 源码和开发文件
packages/
playgrounds/
node_modules/
.vscode/
.idea/# 配置文件
.gitignore
.eslintrc.js
.prettierrc
tsconfig.json
vite.lib.config.ts# 其他文件
*.log
5.2.2 登录NPM
# 登录NPM
npm login
输入用户名、密码和邮箱,如果有双因素认证,还需要输入验证码。
5.2.3 发布包
# 发布包
npm publish
如果是第一次发布,可能需要添加--access=public
参数:
npm publish --access=public
5.2.4 版本更新
当需要更新版本时,修改package.json
中的版本号,然后重新打包和发布:
# 更新版本号
npm version patch # 小版本更新
npm version minor # 中版本更新
npm version major # 大版本更新# 打包
pnpm run build# 发布
npm publish
5.3 使用发布的组件
在其他项目中安装和使用:
# 安装组件
pnpm add @dripadmin/drip-table
在Vue项目中注册和使用:
在main.ts 中引入全局的样式
// main.ts
import "@dripadmin/drip-table/style.css";
然后在业务组件中例如菜单管理中使用:
<template><DripForm:config="formConfig"@submit="onFormSubmit"@reset="onFormReset"@change="onFormChange"/><DripTable:columns="columns":data="rows":pagination="pagination":toolbar-right="toolbarRight":elTableProps="elTableProps":row-toolbar="tableRowToolbar"@page-size-change="onPageSizeChange"@page-current-change="onPageCurrentChange"@refresh="onRefresh"@row-click="onRowClick"><template #titleHeader><span>菜单名称</span></template><template #titleCell="{ row }"><span>{{ row.title }}</span></template></DripTable>
</template><script setup lang="ts">
import { onMounted, ref } from "vue";
import { DripTable,DripForm } from "@dripadmin/drip-table";
import type {DripTableColumn,DripTablePagination,DripTableToolbarConfig,DripTableRowToolBar,DripFormConfig,
} from "@dripadmin/drip-table";
import { getMenuListApi } from "@/api/menu_api";// 列定义
const columns = ref<DripTableColumn[]>([{ type: "index", label: "序", width: 60, align: "center" },{label: "菜单名称",prop: "title",slot: "titleCell",headerSlot: "titleHeader",minWidth: 160,},{ label: "路径", prop: "path", minWidth: 200 },{ label: "图标", prop: "icon", minWidth: 120 },{ label: "类型", prop: "type", minWidth: 100, align: "center" },{ label: "状态", prop: "status", minWidth: 100, align: "center" },{ label: "排序", prop: "order", minWidth: 80, align: "center" },
]);const tableRowToolbar = ref<DripTableRowToolBar>({actions: [{ label: "新增", type: "primary", event: "add" },{ label: "修改", type: "warning", event: "edit" },{ label: "删除", type: "danger", event: "delete" },],
});// 数据与分页
const rows = ref<any[]>([]);
const pagination = ref<DripTablePagination>({total: 0,pageSize: 10,currentPage: 1,
});// 工具条(右侧显示刷新/大小/列设置/最大化)
const toolbarRight = ref<DripTableToolbarConfig>({showRefresh: true,showSize: true,showColumnSetting: true,showFullscreen: true,
});// 透传 el-table 原生属性
const elTableProps = ref<Record<string, any>>({border: true,size: "default",
});// 加载菜单数据(兼容不同返回结构)
async function loadData() {const page = pagination.value.currentPage;const size = pagination.value.pageSize;const res: any = await getMenuListApi({ page, pageSize: size });const list =res?.list ?? res?.records ?? res?.rows ?? (Array.isArray(res) ? res : []);const total = res?.total ?? list.length ?? 0;rows.value = list || [];pagination.value.total = total || 0;
}function onPageSizeChange(size: number) {pagination.value.pageSize = size;pagination.value.currentPage = 1;loadData();
}function onPageCurrentChange(page: number) {pagination.value.currentPage = page;loadData();
}function onRefresh() {pagination.value.currentPage = 1;loadData();
}function onRowClick(eventName: string, row: any) {console.log("点击行操作:", eventName, row);
}onMounted(() => {loadData();
});const formConfig = ref<DripFormConfig>({items: [{type: "input",label: "名称",field: "name",placeholder: "输入名称",width: 220,},{type: "select",label: "类型",field: "type",options: [{ label: "目录", value: "0" },{ label: "页面", value: "1" },{ label: "按钮", value: "2" },{ label: "链接", value: "3" },],width: 140,},{type: "select",label: "状态",field: "status",options: [{ label: "启用", value: "启用" },{ label: "停用", value: "停用" },],width: 140,},],
});// 筛选条件
const filters = ref<{keyword: string;type: string | null;status: string | null;
}>({ keyword: "", type: null, status: null });
function onFormSubmit(values: Record<string, any>) {filters.value = { ...filters.value, ...values } as any;pagination.value.currentPage = 1;loadData();
}
function onFormReset(values: Record<string, any>) {filters.value = { keyword: "", type: null, status: null };pagination.value.currentPage = 1;loadData();
}
function onFormChange(field: string, value: any, values: Record<string, any>) {filters.value = { ...filters.value, ...values } as any;
}</script>
6. 总结与进阶
该组件部分代码使用AI自动生成,再进行加工优化,基本完成了基于Element Plus的表格组件二次封装, 已经发布在NPM库中。
这个组件库可以帮助我们在项目中快速实现简单的表格查询操作,提高开发效率, 后续会继续完善.
做为学习的一部分, 会逐步应用到dripadmin项目中.