Skip to content

第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:创建增强型组件

  1. 基于 ElButton 创建增强型按钮组件
  2. 添加权限控制、防抖、确认等功能
  3. 实现异步操作处理
  4. 编写完整的类型定义

练习 2:开发高级表格组件

  1. 基于 ElTable 创建高级表格组件
  2. 实现搜索、筛选、排序功能
  3. 添加批量操作和行操作
  4. 支持列设置和数据导出

练习 3:构建组件库

  1. 搭建组件库项目结构
  2. 配置构建和打包流程
  3. 实现自动化测试和发布
  4. 编写组件文档和使用指南

学习资源

作业

  1. 完成所有实践练习
  2. 设计并实现一个完整的组件库
  3. 编写组件库的使用文档
  4. 分析组件库的性能优化策略

下一步学习计划

接下来我们将学习 Element Plus 性能监控与分析,了解如何监控和分析 Element Plus 应用的性能,实现性能优化和问题诊断。

Element Plus Study Guide