第73天:Element Plus 组件库二次开发
学习目标
- 掌握基于 Element Plus 的组件库二次开发技术
- 学习组件扩展和增强的最佳实践
- 了解组件库的打包、发布和版本管理
- 实现企业级组件库的构建和维护
知识点概览
1. 组件库架构设计
1.1 组件库基础架构
typescript
// 组件库配置接口
interface ComponentLibraryConfig {
name: string
version: string
description: string
author: string
license: string
repository: string
// 构建配置
build: {
entry: string
output: string
formats: ('es' | 'cjs' | 'umd')[]
external: string[]
globals: Record<string, string>
}
// 组件配置
components: {
prefix: string
namespace: string
stylePrefix: string
iconPrefix: string
}
// 主题配置
theme: {
primary: string
variables: Record<string, string>
darkMode: boolean
}
// 文档配置
docs: {
title: string
description: string
logo: string
favicon: string
baseUrl: string
}
}
// 组件库管理器
class ComponentLibraryManager {
private config: ComponentLibraryConfig
private components: Map<string, ComponentDefinition> = new Map()
private themes: Map<string, ThemeDefinition> = new Map()
private plugins: Map<string, PluginDefinition> = new Map()
constructor(config: ComponentLibraryConfig) {
this.config = config
this.initializeLibrary()
}
// 初始化组件库
private initializeLibrary(): void {
this.loadBaseComponents()
this.loadCustomComponents()
this.loadThemes()
this.setupGlobalConfig()
}
// 加载基础组件(Element Plus)
private loadBaseComponents(): void {
// 从 Element Plus 导入基础组件
const elementComponents = [
'ElButton', 'ElInput', 'ElSelect', 'ElTable',
'ElForm', 'ElDialog', 'ElMessage', 'ElNotification'
]
elementComponents.forEach(name => {
this.components.set(name, {
name,
type: 'base',
source: 'element-plus',
version: '2.4.0'
})
})
}
// 加载自定义组件
private loadCustomComponents(): void {
// 扫描自定义组件目录
const customComponents = this.scanCustomComponents()
customComponents.forEach(component => {
this.registerComponent(component)
})
}
// 扫描自定义组件
private scanCustomComponents(): ComponentDefinition[] {
// 实际实现中会扫描文件系统
return [
{
name: 'MyButton',
type: 'custom',
source: 'local',
version: '1.0.0',
extends: 'ElButton',
path: './components/MyButton'
},
{
name: 'DataTable',
type: 'custom',
source: 'local',
version: '1.0.0',
extends: 'ElTable',
path: './components/DataTable'
}
]
}
// 注册组件
registerComponent(definition: ComponentDefinition): void {
this.components.set(definition.name, definition)
console.log(`Component '${definition.name}' registered successfully`)
}
// 获取组件定义
getComponent(name: string): ComponentDefinition | undefined {
return this.components.get(name)
}
// 获取所有组件
getAllComponents(): ComponentDefinition[] {
return Array.from(this.components.values())
}
// 注册主题
registerTheme(name: string, theme: ThemeDefinition): void {
this.themes.set(name, theme)
}
// 注册插件
registerPlugin(name: string, plugin: PluginDefinition): void {
this.plugins.set(name, plugin)
}
// 设置全局配置
private setupGlobalConfig(): void {
// 设置全局样式变量
const root = document.documentElement
Object.entries(this.config.theme.variables).forEach(([key, value]) => {
root.style.setProperty(`--${this.config.components.stylePrefix}-${key}`, value)
})
}
// 构建组件库
async build(): Promise<void> {
console.log('Building component library...')
// 生成组件索引
await this.generateComponentIndex()
// 生成类型定义
await this.generateTypeDefinitions()
// 生成样式文件
await this.generateStyleFiles()
// 生成文档
await this.generateDocumentation()
console.log('Component library built successfully')
}
// 生成组件索引
private async generateComponentIndex(): Promise<void> {
const components = this.getAllComponents()
const indexContent = this.generateIndexContent(components)
// 写入文件(实际实现中)
console.log('Generated component index:', indexContent)
}
// 生成索引内容
private generateIndexContent(components: ComponentDefinition[]): string {
const imports = components.map(comp =>
`export { default as ${comp.name} } from '${comp.path}'`
).join('\n')
const exports = components.map(comp => comp.name).join(', ')
return `${imports}\n\nexport { ${exports} }`
}
// 生成类型定义
private async generateTypeDefinitions(): Promise<void> {
const components = this.getAllComponents()
const typeContent = this.generateTypeContent(components)
console.log('Generated type definitions:', typeContent)
}
// 生成类型内容
private generateTypeContent(components: ComponentDefinition[]): string {
return components.map(comp => {
return `declare module '${this.config.name}' {
export const ${comp.name}: any
}`
}).join('\n\n')
}
// 生成样式文件
private async generateStyleFiles(): Promise<void> {
const styleContent = this.generateStyleContent()
console.log('Generated style files:', styleContent)
}
// 生成样式内容
private generateStyleContent(): string {
const prefix = this.config.components.stylePrefix
const variables = Object.entries(this.config.theme.variables)
.map(([key, value]) => ` --${prefix}-${key}: ${value};`)
.join('\n')
return `:root {\n${variables}\n}`
}
// 生成文档
private async generateDocumentation(): Promise<void> {
const components = this.getAllComponents()
const docContent = this.generateDocContent(components)
console.log('Generated documentation:', docContent)
}
// 生成文档内容
private generateDocContent(components: ComponentDefinition[]): string {
return components.map(comp => {
return `## ${comp.name}\n\n${comp.description || 'No description available'}\n`
}).join('\n')
}
}
// 组件定义接口
interface ComponentDefinition {
name: string
type: 'base' | 'custom' | 'enhanced'
source: 'element-plus' | 'local' | 'external'
version: string
extends?: string
path?: string
description?: string
props?: Record<string, any>
events?: Record<string, any>
slots?: Record<string, any>
}
// 主题定义接口
interface ThemeDefinition {
name: string
variables: Record<string, string>
colors: Record<string, string>
fonts: Record<string, string>
spacing: Record<string, string>
}
// 插件定义接口
interface PluginDefinition {
name: string
version: string
install: (app: any, options?: any) => void
dependencies?: string[]
}
1.2 组件扩展基类
typescript
// 组件扩展基类
abstract class ComponentExtension<T = any> {
protected baseComponent: any
protected options: T
protected eventBus: EventEmitter
constructor(baseComponent: any, options: T) {
this.baseComponent = baseComponent
this.options = options
this.eventBus = new EventEmitter()
this.initialize()
}
// 初始化扩展
protected abstract initialize(): void
// 扩展属性
protected abstract extendProps(): Record<string, any>
// 扩展事件
protected abstract extendEvents(): Record<string, Function>
// 扩展插槽
protected abstract extendSlots(): Record<string, any>
// 扩展方法
protected abstract extendMethods(): Record<string, Function>
// 生成扩展组件
generateComponent(): any {
const extendedProps = this.extendProps()
const extendedEvents = this.extendEvents()
const extendedSlots = this.extendSlots()
const extendedMethods = this.extendMethods()
return {
name: this.getComponentName(),
extends: this.baseComponent,
props: {
...this.baseComponent.props,
...extendedProps
},
emits: [
...this.baseComponent.emits || [],
...Object.keys(extendedEvents)
],
setup(props: any, { emit, slots, expose }: any) {
// 调用基础组件的 setup
const baseSetup = this.baseComponent.setup?.(props, { emit, slots, expose })
// 扩展功能
const extensions = this.setupExtensions(props, emit, slots)
// 合并并返回
return {
...baseSetup,
...extensions,
...extendedMethods
}
},
render() {
return this.renderComponent()
}
}
}
// 获取组件名称
protected abstract getComponentName(): string
// 设置扩展功能
protected abstract setupExtensions(props: any, emit: Function, slots: any): any
// 渲染组件
protected abstract renderComponent(): any
// 触发事件
protected emit(event: string, ...args: any[]): void {
this.eventBus.emit(event, ...args)
}
// 监听事件
protected on(event: string, callback: Function): void {
this.eventBus.on(event, callback)
}
}
// 事件发射器
class EventEmitter {
private events: Map<string, Set<Function>> = new Map()
on(event: string, callback: Function): void {
if (!this.events.has(event)) {
this.events.set(event, new Set())
}
this.events.get(event)!.add(callback)
}
emit(event: string, ...args: any[]): void {
const callbacks = this.events.get(event)
if (callbacks) {
callbacks.forEach(callback => callback(...args))
}
}
off(event: string, callback?: Function): void {
if (callback) {
this.events.get(event)?.delete(callback)
} else {
this.events.delete(event)
}
}
}
2. 组件扩展实践
2.1 增强型按钮组件
vue
<!-- EnhancedButton.vue -->
<template>
<el-button
v-bind="buttonProps"
:class="buttonClasses"
:disabled="isDisabled"
@click="handleClick"
>
<!-- 加载状态 -->
<template v-if="loading">
<el-icon class="is-loading">
<Loading />
</el-icon>
<span v-if="loadingText">{{ loadingText }}</span>
</template>
<!-- 正常状态 -->
<template v-else>
<!-- 前置图标 -->
<el-icon v-if="prefixIcon" class="prefix-icon">
<component :is="prefixIcon" />
</el-icon>
<!-- 按钮内容 -->
<slot>
{{ text }}
</slot>
<!-- 后置图标 -->
<el-icon v-if="suffixIcon" class="suffix-icon">
<component :is="suffixIcon" />
</el-icon>
<!-- 徽章 -->
<el-badge
v-if="badge"
:value="badge.value"
:type="badge.type"
:is-dot="badge.isDot"
class="button-badge"
/>
</template>
</el-button>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { ElButton, ElIcon, ElBadge } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
// 属性定义
interface EnhancedButtonProps {
// 继承 ElButton 的所有属性
size?: 'large' | 'default' | 'small'
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text'
plain?: boolean
round?: boolean
circle?: boolean
disabled?: boolean
// 扩展属性
text?: string
prefixIcon?: string | Component
suffixIcon?: string | Component
loading?: boolean
loadingText?: string
// 徽章配置
badge?: {
value: string | number
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info'
isDot?: boolean
}
// 权限控制
permission?: string | string[]
// 防抖配置
debounce?: number
// 确认配置
confirm?: {
title: string
message?: string
type?: 'warning' | 'info' | 'success' | 'error'
}
// 异步操作
asyncHandler?: () => Promise<any>
}
// 事件定义
interface EnhancedButtonEmits {
click: [event: MouseEvent]
asyncStart: []
asyncSuccess: [result: any]
asyncError: [error: any]
asyncComplete: []
}
const props = withDefaults(defineProps<EnhancedButtonProps>(), {
size: 'default',
type: 'primary',
debounce: 300
})
const emit = defineEmits<EnhancedButtonEmits>()
// 状态管理
const isLoading = ref(false)
const lastClickTime = ref(0)
// 计算属性
const buttonProps = computed(() => {
const { text, prefixIcon, suffixIcon, loading, loadingText, badge, permission, debounce, confirm, asyncHandler, ...rest } = props
return rest
})
const buttonClasses = computed(() => {
return [
'enhanced-button',
{
'has-badge': props.badge,
'is-loading': isLoading.value || props.loading
}
]
})
const isDisabled = computed(() => {
return props.disabled || isLoading.value || !hasPermission.value
})
// 权限检查
const hasPermission = computed(() => {
if (!props.permission) return true
// 这里应该调用实际的权限检查逻辑
return checkPermission(props.permission)
})
// 权限检查函数(示例)
function checkPermission(permission: string | string[]): boolean {
// 实际实现中应该从用户权限中检查
const userPermissions = ['read', 'write', 'delete'] // 示例权限
if (Array.isArray(permission)) {
return permission.some(p => userPermissions.includes(p))
}
return userPermissions.includes(permission)
}
// 防抖处理
function debounceClick(callback: Function): void {
const now = Date.now()
if (now - lastClickTime.value < props.debounce) {
return
}
lastClickTime.value = now
callback()
}
// 确认对话框
async function showConfirm(): Promise<boolean> {
if (!props.confirm) return true
try {
// 这里应该调用实际的确认对话框
const { ElMessageBox } = await import('element-plus')
await ElMessageBox.confirm(
props.confirm.message || '确定要执行此操作吗?',
props.confirm.title,
{
type: props.confirm.type || 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
}
)
return true
} catch {
return false
}
}
// 处理异步操作
async function handleAsyncOperation(): Promise<void> {
if (!props.asyncHandler) return
isLoading.value = true
emit('asyncStart')
try {
const result = await props.asyncHandler()
emit('asyncSuccess', result)
} catch (error) {
emit('asyncError', error)
console.error('Async operation failed:', error)
} finally {
isLoading.value = false
emit('asyncComplete')
}
}
// 点击处理
async function handleClick(event: MouseEvent): Promise<void> {
debounceClick(async () => {
// 权限检查
if (!hasPermission.value) {
console.warn('No permission to perform this action')
return
}
// 确认检查
if (props.confirm) {
const confirmed = await showConfirm()
if (!confirmed) return
}
// 发射点击事件
emit('click', event)
// 处理异步操作
if (props.asyncHandler) {
await handleAsyncOperation()
}
})
}
// 监听加载状态变化
watch(() => props.loading, (newVal) => {
if (newVal !== undefined) {
isLoading.value = newVal
}
})
</script>
<style lang="scss" scoped>
.enhanced-button {
position: relative;
.prefix-icon,
.suffix-icon {
margin: 0 4px;
}
.button-badge {
position: absolute;
top: -8px;
right: -8px;
}
&.is-loading {
pointer-events: none;
.is-loading {
animation: rotating 2s linear infinite;
}
}
&.has-badge {
margin-right: 8px;
}
}
@keyframes rotating {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
2.2 高级数据表格组件
vue
<!-- AdvancedTable.vue -->
<template>
<div class="advanced-table">
<!-- 表格工具栏 -->
<div v-if="showToolbar" class="table-toolbar">
<div class="toolbar-left">
<!-- 批量操作 -->
<el-dropdown
v-if="batchActions.length > 0 && selectedRows.length > 0"
@command="handleBatchAction"
>
<el-button type="primary">
批量操作 ({{ selectedRows.length }})
<el-icon><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="action in batchActions"
:key="action.key"
:command="action.key"
:disabled="action.disabled"
>
<el-icon v-if="action.icon">
<component :is="action.icon" />
</el-icon>
{{ action.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 自定义工具栏按钮 -->
<slot name="toolbar-left" :selected-rows="selectedRows" />
</div>
<div class="toolbar-right">
<!-- 搜索框 -->
<el-input
v-if="searchable"
v-model="searchKeyword"
placeholder="搜索..."
:prefix-icon="Search"
clearable
@input="handleSearch"
class="search-input"
/>
<!-- 列设置 -->
<el-popover
v-if="columnSettable"
placement="bottom-end"
:width="300"
trigger="click"
>
<template #reference>
<el-button :icon="Setting" circle />
</template>
<div class="column-settings">
<div class="settings-header">
<span>列设置</span>
<el-button
type="text"
size="small"
@click="resetColumns"
>
重置
</el-button>
</div>
<el-checkbox-group v-model="visibleColumns">
<div
v-for="column in allColumns"
:key="column.prop"
class="column-item"
>
<el-checkbox :label="column.prop">
{{ column.label }}
</el-checkbox>
</div>
</el-checkbox-group>
</div>
</el-popover>
<!-- 刷新按钮 -->
<el-button
v-if="refreshable"
:icon="Refresh"
:loading="loading"
circle
@click="handleRefresh"
/>
<!-- 自定义工具栏按钮 -->
<slot name="toolbar-right" :selected-rows="selectedRows" />
</div>
</div>
<!-- 表格主体 -->
<el-table
ref="tableRef"
v-bind="tableProps"
:data="filteredData"
:loading="loading"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
@filter-change="handleFilterChange"
class="main-table"
>
<!-- 选择列 -->
<el-table-column
v-if="selectable"
type="selection"
width="55"
:selectable="selectableFunction"
/>
<!-- 序号列 -->
<el-table-column
v-if="showIndex"
type="index"
label="序号"
width="80"
:index="indexMethod"
/>
<!-- 动态列 -->
<template v-for="column in displayColumns" :key="column.prop">
<el-table-column
v-bind="column"
:show-overflow-tooltip="column.showOverflowTooltip ?? true"
>
<!-- 自定义列头 -->
<template v-if="column.headerSlot" #header="scope">
<slot :name="`header-${column.prop}`" v-bind="scope">
{{ column.label }}
</slot>
</template>
<!-- 自定义列内容 -->
<template #default="scope">
<slot
v-if="column.slot"
:name="column.prop"
v-bind="scope"
>
{{ getCellValue(scope.row, column.prop) }}
</slot>
<!-- 默认渲染 -->
<template v-else>
<component
v-if="column.component"
:is="column.component"
v-bind="column.componentProps"
:value="getCellValue(scope.row, column.prop)"
:row="scope.row"
:column="column"
:index="scope.$index"
/>
<span v-else>
{{ formatCellValue(scope.row, column) }}
</span>
</template>
</template>
</el-table-column>
</template>
<!-- 操作列 -->
<el-table-column
v-if="actions.length > 0"
label="操作"
:width="actionColumnWidth"
fixed="right"
>
<template #default="scope">
<div class="action-buttons">
<template v-for="action in getRowActions(scope.row)" :key="action.key">
<el-button
v-if="action.type === 'button'"
:type="action.buttonType || 'text'"
:size="action.size || 'small'"
:disabled="action.disabled"
@click="handleAction(action, scope.row, scope.$index)"
>
<el-icon v-if="action.icon">
<component :is="action.icon" />
</el-icon>
{{ action.label }}
</el-button>
<el-dropdown
v-else-if="action.type === 'dropdown'"
@command="(command) => handleAction(command, scope.row, scope.$index)"
>
<el-button type="text" size="small">
{{ action.label }}
<el-icon><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in action.items"
:key="item.key"
:command="item"
:disabled="item.disabled"
>
<el-icon v-if="item.icon">
<component :is="item.icon" />
</el-icon>
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页器 -->
<div v-if="pagination" class="table-pagination">
<el-pagination
v-bind="paginationProps"
:current-page="currentPage"
:page-size="pageSize"
:total="total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'
import { ElTable, ElTableColumn, ElButton, ElDropdown, ElDropdownMenu, ElDropdownItem, ElInput, ElPopover, ElCheckbox, ElCheckboxGroup, ElPagination, ElIcon } from 'element-plus'
import { ArrowDown, Search, Setting, Refresh } from '@element-plus/icons-vue'
// 类型定义
interface TableColumn {
prop: string
label: string
width?: string | number
minWidth?: string | number
fixed?: boolean | 'left' | 'right'
sortable?: boolean | 'custom'
filterable?: boolean
filters?: Array<{ text: string; value: any }>
formatter?: (row: any, column: any, cellValue: any, index: number) => string
showOverflowTooltip?: boolean
slot?: boolean
headerSlot?: boolean
component?: any
componentProps?: Record<string, any>
}
interface TableAction {
key: string
label: string
type: 'button' | 'dropdown'
icon?: any
buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text'
size?: 'large' | 'default' | 'small'
disabled?: boolean | ((row: any) => boolean)
visible?: boolean | ((row: any) => boolean)
items?: TableAction[] // for dropdown
handler?: (row: any, index: number) => void
}
interface BatchAction {
key: string
label: string
icon?: any
disabled?: boolean
handler?: (rows: any[]) => void
}
// 属性定义
interface AdvancedTableProps {
data: any[]
columns: TableColumn[]
loading?: boolean
// 表格配置
height?: string | number
maxHeight?: string | number
stripe?: boolean
border?: boolean
size?: 'large' | 'default' | 'small'
// 功能配置
selectable?: boolean
selectableFunction?: (row: any, index: number) => boolean
showIndex?: boolean
indexMethod?: (index: number) => number
// 工具栏配置
showToolbar?: boolean
searchable?: boolean
columnSettable?: boolean
refreshable?: boolean
// 操作配置
actions?: TableAction[]
batchActions?: BatchAction[]
actionColumnWidth?: string | number
// 分页配置
pagination?: boolean
currentPage?: number
pageSize?: number
total?: number
pageSizes?: number[]
// 其他配置
emptyText?: string
defaultSort?: { prop: string; order: 'ascending' | 'descending' }
}
// 事件定义
interface AdvancedTableEmits {
'selection-change': [selection: any[]]
'sort-change': [{ column: any; prop: string; order: string }]
'filter-change': [filters: Record<string, any>]
'current-change': [currentPage: number]
'size-change': [pageSize: number]
'refresh': []
'search': [keyword: string]
'action': [action: TableAction, row: any, index: number]
'batch-action': [action: BatchAction, rows: any[]]
}
const props = withDefaults(defineProps<AdvancedTableProps>(), {
loading: false,
stripe: true,
border: true,
size: 'default',
selectable: false,
showIndex: false,
showToolbar: true,
searchable: true,
columnSettable: true,
refreshable: true,
actions: () => [],
batchActions: () => [],
actionColumnWidth: 150,
pagination: true,
currentPage: 1,
pageSize: 20,
total: 0,
pageSizes: () => [10, 20, 50, 100],
emptyText: '暂无数据'
})
const emit = defineEmits<AdvancedTableEmits>()
// 引用
const tableRef = ref<InstanceType<typeof ElTable>>()
// 状态管理
const selectedRows = ref<any[]>([])
const searchKeyword = ref('')
const visibleColumns = ref<string[]>([])
const allColumns = ref<TableColumn[]>([])
const sortConfig = ref<{ prop: string; order: string } | null>(null)
const filterConfig = ref<Record<string, any>>({})
// 计算属性
const tableProps = computed(() => {
const { data, columns, actions, batchActions, showToolbar, searchable, columnSettable, refreshable, pagination, ...rest } = props
return rest
})
const paginationProps = computed(() => {
return {
background: true,
layout: 'total, sizes, prev, pager, next, jumper',
pageSizes: props.pageSizes
}
})
const displayColumns = computed(() => {
return allColumns.value.filter(column =>
visibleColumns.value.includes(column.prop)
)
})
const filteredData = computed(() => {
let result = [...props.data]
// 搜索过滤
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase()
result = result.filter(row => {
return Object.values(row).some(value =>
String(value).toLowerCase().includes(keyword)
)
})
}
// 列过滤
Object.entries(filterConfig.value).forEach(([prop, values]) => {
if (values && values.length > 0) {
result = result.filter(row => values.includes(row[prop]))
}
})
// 排序
if (sortConfig.value) {
const { prop, order } = sortConfig.value
result.sort((a, b) => {
const aVal = getCellValue(a, prop)
const bVal = getCellValue(b, prop)
if (order === 'ascending') {
return aVal > bVal ? 1 : -1
} else {
return aVal < bVal ? 1 : -1
}
})
}
return result
})
// 初始化
function initialize(): void {
allColumns.value = [...props.columns]
visibleColumns.value = props.columns.map(col => col.prop)
if (props.defaultSort) {
sortConfig.value = props.defaultSort
}
}
// 获取单元格值
function getCellValue(row: any, prop: string): any {
return prop.split('.').reduce((obj, key) => obj?.[key], row)
}
// 格式化单元格值
function formatCellValue(row: any, column: TableColumn): string {
const value = getCellValue(row, column.prop)
if (column.formatter) {
return column.formatter(row, column, value, 0)
}
return String(value ?? '')
}
// 获取行操作
function getRowActions(row: any): TableAction[] {
return props.actions.filter(action => {
if (typeof action.visible === 'function') {
return action.visible(row)
}
return action.visible !== false
})
}
// 事件处理
function handleSelectionChange(selection: any[]): void {
selectedRows.value = selection
emit('selection-change', selection)
}
function handleSortChange({ column, prop, order }: any): void {
sortConfig.value = order ? { prop, order } : null
emit('sort-change', { column, prop, order })
}
function handleFilterChange(filters: Record<string, any>): void {
filterConfig.value = filters
emit('filter-change', filters)
}
function handleCurrentChange(currentPage: number): void {
emit('current-change', currentPage)
}
function handleSizeChange(pageSize: number): void {
emit('size-change', pageSize)
}
function handleRefresh(): void {
emit('refresh')
}
function handleSearch(): void {
emit('search', searchKeyword.value)
}
function handleAction(action: TableAction, row: any, index: number): void {
if (action.handler) {
action.handler(row, index)
}
emit('action', action, row, index)
}
function handleBatchAction(actionKey: string): void {
const action = props.batchActions.find(a => a.key === actionKey)
if (action) {
if (action.handler) {
action.handler(selectedRows.value)
}
emit('batch-action', action, selectedRows.value)
}
}
function resetColumns(): void {
visibleColumns.value = allColumns.value.map(col => col.prop)
}
// 公开方法
function clearSelection(): void {
tableRef.value?.clearSelection()
}
function toggleRowSelection(row: any, selected?: boolean): void {
tableRef.value?.toggleRowSelection(row, selected)
}
function toggleAllSelection(): void {
tableRef.value?.toggleAllSelection()
}
function setCurrentRow(row: any): void {
tableRef.value?.setCurrentRow(row)
}
function sort(prop: string, order: string): void {
tableRef.value?.sort(prop, order)
}
// 暴露方法
defineExpose({
clearSelection,
toggleRowSelection,
toggleAllSelection,
setCurrentRow,
sort,
tableRef
})
// 初始化
initialize()
// 监听列变化
watch(() => props.columns, () => {
initialize()
}, { deep: true })
</script>
<style lang="scss" scoped>
.advanced-table {
.table-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.search-input {
width: 200px;
}
}
.main-table {
.action-buttons {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
}
.table-pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.column-settings {
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.column-item {
margin-bottom: 8px;
}
}
}
</style>
3. 组件库构建与发布
3.1 构建配置
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [
vue(),
dts({
insertTypesEntry: true,
cleanVueFileName: true,
skipDiagnostics: false,
tsConfigFilePath: './tsconfig.json'
})
],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyComponentLibrary',
fileName: (format) => `my-component-library.${format}.js`,
formats: ['es', 'cjs', 'umd']
},
rollupOptions: {
external: [
'vue',
'element-plus',
'@element-plus/icons-vue'
],
output: {
globals: {
vue: 'Vue',
'element-plus': 'ElementPlus',
'@element-plus/icons-vue': 'ElementPlusIconsVue'
},
assetFileNames: (assetInfo) => {
if (assetInfo.name === 'style.css') {
return 'my-component-library.css'
}
return assetInfo.name
}
}
},
cssCodeSplit: false,
sourcemap: true,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
}
})
json
// package.json
{
"name": "my-component-library",
"version": "1.0.0",
"description": "基于 Element Plus 的企业级组件库",
"main": "dist/my-component-library.cjs.js",
"module": "dist/my-component-library.es.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"README.md"
],
"exports": {
".": {
"import": "./dist/my-component-library.es.js",
"require": "./dist/my-component-library.cjs.js",
"types": "./dist/index.d.ts"
},
"./dist/style.css": "./dist/my-component-library.css"
},
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"build:docs": "vitepress build docs",
"preview": "vite preview",
"test": "vitest",
"test:coverage": "vitest --coverage",
"lint": "eslint src --ext .vue,.js,.ts",
"lint:fix": "eslint src --ext .vue,.js,.ts --fix",
"type-check": "vue-tsc --noEmit",
"release": "npm run build && npm publish",
"prepublishOnly": "npm run build && npm run test"
},
"peerDependencies": {
"vue": "^3.3.0",
"element-plus": "^2.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.4.0",
"@vue/tsconfig": "^0.4.0",
"typescript": "^5.2.0",
"vite": "^4.4.0",
"vite-plugin-dts": "^3.6.0",
"vue-tsc": "^1.8.0",
"vitest": "^0.34.0",
"@vitest/coverage-v8": "^0.34.0",
"eslint": "^8.50.0",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
"eslint-plugin-vue": "^9.17.0"
},
"keywords": [
"vue",
"vue3",
"element-plus",
"component-library",
"ui",
"typescript"
],
"author": "Your Name",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/your-username/my-component-library.git"
},
"bugs": {
"url": "https://github.com/your-username/my-component-library/issues"
},
"homepage": "https://github.com/your-username/my-component-library#readme"
}
3.2 自动化发布流程
yaml
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test
- name: Build library
run: npm run build
- name: Build documentation
run: npm run build:docs
- name: Publish to NPM
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Deploy documentation
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/.vitepress/dist
实践练习
练习 1:创建增强型组件
- 基于 ElButton 创建增强型按钮组件
- 添加权限控制、防抖、确认等功能
- 实现异步操作处理
- 编写完整的类型定义
练习 2:开发高级表格组件
- 基于 ElTable 创建高级表格组件
- 实现搜索、筛选、排序功能
- 添加批量操作和行操作
- 支持列设置和数据导出
练习 3:构建组件库
- 搭建组件库项目结构
- 配置构建和打包流程
- 实现自动化测试和发布
- 编写组件文档和使用指南
学习资源
作业
- 完成所有实践练习
- 设计并实现一个完整的组件库
- 编写组件库的使用文档
- 分析组件库的性能优化策略
下一步学习计划
接下来我们将学习 Element Plus 性能监控与分析,了解如何监控和分析 Element Plus 应用的性能,实现性能优化和问题诊断。