当前组件已经发布到npm库中,使用 pnpm add @dripadmin/drip-table 即可安装
表格本身是透明的, 实际使用时,需要自己设置背景色.
文章目录
-
- [1. 需求分析](#1. 需求分析)
- [2. 项目初始化与架构设计](#2. 项目初始化与架构设计)
-
- [2.1 项目初始化](#2.1 项目初始化)
- [2.2 目录结构设计](#2.2 目录结构设计)
- [2.3 基础配置文件](#2.3 基础配置文件)
- [3. 组件设计与实现](#3. 组件设计与实现)
- [4. 组件功能与API文档](#4. 组件功能与API文档)
- [5. 打包与发布](#5. 打包与发布)
-
- [5.1 打包组件库](#5.1 打包组件库)
- [5.2 发布到NPM](#5.2 发布到NPM)
-
- [5.2.1 准备发布](#5.2.1 准备发布)
- [5.2.2 登录NPM](#5.2.2 登录NPM)
- [5.2.3 发布包](#5.2.3 发布包)
- [5.2.4 版本更新](#5.2.4 版本更新)
- [5.3 使用发布的组件](#5.3 使用发布的组件)
- [6. 总结与进阶](#6. 总结与进阶)
1. 需求分析
Element Plus提供了功能强大的el-table
组件,但在实际业务系统中,存在大量重复的增删改查的操作,
所以针对现有的el-table进行封装后,只需要提供json类数据, 即可实现表单表格的操作, 提高我们的效率,
所需求大致如下:
- 简化配置:通过JSON配置生成复杂表格
- 工具栏:内置表格工具栏,支持自定义按钮和操作
- 行操作:支持行级别的操作按钮
- 分页控制:集成分页功能
- 自定义渲染:支持自定义列渲染
- 国际化:支持多语言
- 主题定制:支持自定义主题
- TypeScript支持:完整的类型定义
整体实现的效果如下图:
2. 项目初始化与架构设计
2.1 项目初始化
首先,我们需要创建一个基于Vue3和TypeScript的组件库项目:
bash
# 创建项目目录
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
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
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
typescript
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 类型定义
首先,我们需要定义组件的类型:
typescript
// 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 组件实现
主组件实现
vue
<!-- packages/components/drip-table/index.vue -->
<template>
<ElConfigProvider :locale="elementLocale">
<div
class="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">
<Toolbar
v-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">
<Toolbar
v-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>
<!-- 表格 -->
<ElTable
ref="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">
<ElTableColumn
v-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">
<ElPagination
v-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>
行操作工具栏组件
vue
<!-- packages/components/drip-table/row-toolbar/index.vue -->
<template>
<div class="drip-table-row-toolbar">
<el-button-group v-if="group">
<el-button
v-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-button
v-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 入口文件
typescript
// 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 行操作工具栏配置
javascript
// 行操作工具栏配置示例
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 打包组件库
bash
# 执行打包命令
pnpm run build
打包后的文件将输出到dist
目录,包含以下文件:
drip-table.es.js
- ES模块格式drip-table.umd.js
- UMD格式types/
- TypeScript类型定义
5.2 发布到NPM
5.2.1 准备发布
- 确保
package.json
中的信息正确:
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
bash
# 登录NPM
npm login
输入用户名、密码和邮箱,如果有双因素认证,还需要输入验证码。
5.2.3 发布包
bash
# 发布包
npm publish
如果是第一次发布,可能需要添加--access=public
参数:
bash
npm publish --access=public
5.2.4 版本更新
当需要更新版本时,修改package.json
中的版本号,然后重新打包和发布:
bash
# 更新版本号
npm version patch # 小版本更新
npm version minor # 中版本更新
npm version major # 大版本更新
# 打包
pnpm run build
# 发布
npm publish
5.3 使用发布的组件
在其他项目中安装和使用:
bash
# 安装组件
pnpm add @dripadmin/drip-table
在Vue项目中注册和使用:
在main.ts 中引入全局的样式
javascript
// main.ts
import "@dripadmin/drip-table/style.css";
然后在业务组件中例如菜单管理中使用:
vue
<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项目中.