Skip to content

第58天:Element Plus SSR 综合实践

学习目标

  • 综合运用 Element Plus SSR 的所有技术栈
  • 构建完整的企业级 SSR 应用
  • 掌握复杂业务场景的 SSR 解决方案
  • 实现高性能、可扩展的 SSR 架构

知识点概览

1. 项目架构设计

1.1 整体架构规划

typescript
// types/architecture.ts
export interface SSRArchitecture {
  // 前端层
  frontend: {
    framework: 'Vue 3 + TypeScript'
    ui: 'Element Plus'
    routing: 'Vue Router 4'
    state: 'Pinia'
    styling: 'SCSS + CSS Variables'
    build: 'Vite'
  }
  
  // SSR 层
  ssr: {
    renderer: 'Vue Server Renderer'
    hydration: 'Progressive Hydration'
    caching: 'Multi-level Caching'
    optimization: 'Code Splitting + Lazy Loading'
  }
  
  // 服务层
  server: {
    runtime: 'Node.js + Express'
    clustering: 'PM2 Cluster'
    proxy: 'Nginx'
    loadBalancer: 'Kubernetes'
  }
  
  // 数据层
  data: {
    api: 'RESTful + GraphQL'
    cache: 'Redis Cluster'
    database: 'PostgreSQL'
    cdn: 'CloudFlare'
  }
  
  // 监控层
  monitoring: {
    apm: 'New Relic'
    logging: 'ELK Stack'
    metrics: 'Prometheus + Grafana'
    alerts: 'PagerDuty'
  }
  
  // 部署层
  deployment: {
    containerization: 'Docker'
    orchestration: 'Kubernetes'
    cicd: 'GitHub Actions'
    infrastructure: 'AWS/Azure/GCP'
  }
}

// 项目结构
export interface ProjectStructure {
  src: {
    components: 'UI 组件'
    composables: 'Vue 组合式函数'
    layouts: '布局组件'
    pages: '页面组件'
    plugins: '插件配置'
    stores: 'Pinia 状态管理'
    styles: '样式文件'
    utils: '工具函数'
    types: 'TypeScript 类型定义'
  }
  
  server: {
    api: 'API 路由'
    middleware: '中间件'
    services: '业务服务'
    cache: '缓存层'
    config: '配置文件'
    utils: '服务端工具'
  }
  
  build: {
    client: '客户端构建配置'
    server: '服务端构建配置'
    shared: '共享构建配置'
  }
  
  deployment: {
    docker: 'Docker 配置'
    kubernetes: 'K8s 配置'
    scripts: '部署脚本'
    monitoring: '监控配置'
  }
}

1.2 核心配置系统

typescript
// config/app.config.ts
import { defineConfig } from './types'

export default defineConfig({
  // 应用基础配置
  app: {
    name: 'Element Plus SSR Enterprise',
    version: '1.0.0',
    description: '基于 Element Plus 的企业级 SSR 应用',
    author: 'Your Team',
    homepage: 'https://example.com'
  },
  
  // 服务器配置
  server: {
    host: process.env.HOST || '0.0.0.0',
    port: parseInt(process.env.PORT || '3000'),
    workers: parseInt(process.env.WORKERS || '4'),
    timeout: parseInt(process.env.TIMEOUT || '30000'),
    maxMemory: process.env.MAX_MEMORY || '2GB'
  },
  
  // SSR 配置
  ssr: {
    enabled: process.env.SSR_ENABLED !== 'false',
    hydration: {
      strategy: 'progressive',
      chunkSize: 50,
      delay: 100
    },
    prerender: {
      enabled: process.env.PRERENDER_ENABLED === 'true',
      routes: ['/about', '/contact', '/privacy'],
      concurrency: 4
    }
  },
  
  // 缓存配置
  cache: {
    page: {
      enabled: true,
      ttl: 3600000, // 1 hour
      maxSize: 1000
    },
    component: {
      enabled: true,
      ttl: 1800000, // 30 minutes
      maxSize: 5000
    },
    data: {
      enabled: true,
      ttl: 600000, // 10 minutes
      redis: {
        host: process.env.REDIS_HOST || 'localhost',
        port: parseInt(process.env.REDIS_PORT || '6379'),
        password: process.env.REDIS_PASSWORD,
        cluster: process.env.REDIS_CLUSTER === 'true'
      }
    }
  },
  
  // API 配置
  api: {
    baseURL: process.env.API_BASE_URL || 'https://api.example.com',
    timeout: parseInt(process.env.API_TIMEOUT || '10000'),
    retries: parseInt(process.env.API_RETRIES || '3'),
    rateLimit: {
      windowMs: 60000,
      max: 100
    }
  },
  
  // 安全配置
  security: {
    cors: {
      origin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'],
      credentials: true
    },
    helmet: {
      contentSecurityPolicy: {
        directives: {
          defaultSrc: ["'self'"],
          styleSrc: ["'self'", "'unsafe-inline'", 'fonts.googleapis.com'],
          fontSrc: ["'self'", 'fonts.gstatic.com'],
          imgSrc: ["'self'", 'data:', 'https:'],
          scriptSrc: ["'self'", "'unsafe-eval'"]
        }
      }
    },
    rateLimit: {
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100 // limit each IP to 100 requests per windowMs
    }
  },
  
  // 监控配置
  monitoring: {
    enabled: process.env.NODE_ENV === 'production',
    apm: {
      serviceName: 'element-plus-ssr',
      environment: process.env.NODE_ENV || 'development',
      serverUrl: process.env.APM_SERVER_URL
    },
    logging: {
      level: process.env.LOG_LEVEL || 'info',
      format: 'json',
      transports: {
        console: true,
        file: process.env.NODE_ENV === 'production',
        elasticsearch: process.env.ELASTICSEARCH_URL ? true : false
      }
    }
  },
  
  // 性能配置
  performance: {
    compression: {
      enabled: true,
      level: 6,
      threshold: 1024
    },
    static: {
      maxAge: 31536000, // 1 year
      etag: true,
      lastModified: true
    },
    bundleAnalyzer: {
      enabled: process.env.ANALYZE === 'true'
    }
  },
  
  // 开发配置
  development: {
    hmr: true,
    overlay: true,
    devtools: true,
    sourcemap: true
  }
})

2. 企业级组件系统

2.1 高级数据表格组件

vue
<!-- components/DataTable/DataTable.vue -->
<template>
  <div class="data-table">
    <!-- 表格工具栏 -->
    <div class="data-table__toolbar">
      <div class="data-table__toolbar-left">
        <slot name="toolbar-left">
          <el-button
            v-if="showRefresh"
            :icon="Refresh"
            @click="handleRefresh"
          >
            刷新
          </el-button>
          
          <el-button
            v-if="showExport"
            :icon="Download"
            @click="handleExport"
          >
            导出
          </el-button>
        </slot>
      </div>
      
      <div class="data-table__toolbar-right">
        <slot name="toolbar-right">
          <!-- 搜索框 -->
          <el-input
            v-if="showSearch"
            v-model="searchKeyword"
            :placeholder="searchPlaceholder"
            :prefix-icon="Search"
            clearable
            @input="handleSearch"
            style="width: 300px; margin-right: 12px;"
          />
          
          <!-- 列设置 -->
          <el-dropdown v-if="showColumnSettings" trigger="click">
            <el-button :icon="Setting" />
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item
                  v-for="column in configurableColumns"
                  :key="column.prop"
                  @click="toggleColumn(column)"
                >
                  <el-checkbox
                    :model-value="column.visible"
                    @change="toggleColumn(column)"
                  >
                    {{ column.label }}
                  </el-checkbox>
                </el-dropdown-item>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
        </slot>
      </div>
    </div>
    
    <!-- 表格主体 -->
    <el-table
      ref="tableRef"
      v-loading="loading"
      :data="tableData"
      :height="height"
      :max-height="maxHeight"
      :stripe="stripe"
      :border="border"
      :size="size"
      :row-key="rowKey"
      :tree-props="treeProps"
      :expand-row-keys="expandRowKeys"
      :default-sort="defaultSort"
      :highlight-current-row="highlightCurrentRow"
      :row-class-name="rowClassName"
      :cell-class-name="cellClassName"
      :span-method="spanMethod"
      @selection-change="handleSelectionChange"
      @current-change="handleCurrentChange"
      @sort-change="handleSortChange"
      @expand-change="handleExpandChange"
      @row-click="handleRowClick"
      @row-dblclick="handleRowDblclick"
      @row-contextmenu="handleRowContextmenu"
    >
      <!-- 选择列 -->
      <el-table-column
        v-if="showSelection"
        type="selection"
        :width="selectionWidth"
        :fixed="selectionFixed"
        :selectable="selectable"
      />
      
      <!-- 序号列 -->
      <el-table-column
        v-if="showIndex"
        type="index"
        :label="indexLabel"
        :width="indexWidth"
        :fixed="indexFixed"
        :index="indexMethod"
      />
      
      <!-- 展开列 -->
      <el-table-column
        v-if="showExpand"
        type="expand"
        :width="expandWidth"
        :fixed="expandFixed"
      >
        <template #default="{ row, $index }">
          <slot name="expand" :row="row" :index="$index" />
        </template>
      </el-table-column>
      
      <!-- 数据列 -->
      <template v-for="column in visibleColumns" :key="column.prop">
        <el-table-column
          v-if="!column.children"
          :prop="column.prop"
          :label="column.label"
          :width="column.width"
          :min-width="column.minWidth"
          :fixed="column.fixed"
          :sortable="column.sortable"
          :sort-method="column.sortMethod"
          :sort-by="column.sortBy"
          :sort-orders="column.sortOrders"
          :resizable="column.resizable"
          :formatter="column.formatter"
          :show-overflow-tooltip="column.showOverflowTooltip"
          :align="column.align"
          :header-align="column.headerAlign"
          :class-name="column.className"
          :label-class-name="column.labelClassName"
          :filters="column.filters"
          :filter-method="column.filterMethod"
          :filter-multiple="column.filterMultiple"
          :filter-placement="column.filterPlacement"
        >
          <template v-if="column.headerSlot" #header="scope">
            <slot :name="column.headerSlot" v-bind="scope" />
          </template>
          
          <template #default="scope">
            <slot
              v-if="column.slot"
              :name="column.slot"
              v-bind="scope"
            />
            <component
              v-else-if="column.component"
              :is="column.component"
              v-bind="{ ...scope, ...column.componentProps }"
              @click="(event) => handleCellClick(event, scope, column)"
            />
            <span v-else>
              {{ formatCellValue(scope.row, column) }}
            </span>
          </template>
        </el-table-column>
        
        <!-- 多级表头 -->
        <el-table-column
          v-else
          :label="column.label"
          :align="column.align"
          :header-align="column.headerAlign"
        >
          <template v-for="child in column.children" :key="child.prop">
            <el-table-column
              :prop="child.prop"
              :label="child.label"
              :width="child.width"
              :min-width="child.minWidth"
              :fixed="child.fixed"
              :sortable="child.sortable"
              :formatter="child.formatter"
              :show-overflow-tooltip="child.showOverflowTooltip"
              :align="child.align"
              :header-align="child.headerAlign"
            >
              <template v-if="child.headerSlot" #header="scope">
                <slot :name="child.headerSlot" v-bind="scope" />
              </template>
              
              <template #default="scope">
                <slot
                  v-if="child.slot"
                  :name="child.slot"
                  v-bind="scope"
                />
                <component
                  v-else-if="child.component"
                  :is="child.component"
                  v-bind="{ ...scope, ...child.componentProps }"
                />
                <span v-else>
                  {{ formatCellValue(scope.row, child) }}
                </span>
              </template>
            </el-table-column>
          </template>
        </el-table-column>
      </template>
      
      <!-- 操作列 -->
      <el-table-column
        v-if="showActions"
        :label="actionsLabel"
        :width="actionsWidth"
        :min-width="actionsMinWidth"
        :fixed="actionsFixed"
        :align="actionsAlign"
        class-name="data-table__actions"
      >
        <template #default="scope">
          <slot name="actions" v-bind="scope">
            <el-button
              v-for="action in getRowActions(scope.row)"
              :key="action.key"
              :type="action.type"
              :size="action.size || 'small'"
              :icon="action.icon"
              :disabled="action.disabled"
              :loading="action.loading"
              @click="handleActionClick(action, scope)"
            >
              {{ action.label }}
            </el-button>
          </slot>
        </template>
      </el-table-column>
    </el-table>
    
    <!-- 分页器 -->
    <div v-if="showPagination" class="data-table__pagination">
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :total="total"
        :page-sizes="pageSizes"
        :layout="paginationLayout"
        :background="paginationBackground"
        :small="paginationSmall"
        @size-change="handleSizeChange"
        @current-change="handleCurrentPageChange"
      />
    </div>
    
    <!-- 批量操作栏 -->
    <div v-if="showBatchActions && selectedRows.length > 0" class="data-table__batch-actions">
      <div class="data-table__batch-info">
        已选择 {{ selectedRows.length }} 项
        <el-button type="text" @click="clearSelection">清空</el-button>
      </div>
      
      <div class="data-table__batch-buttons">
        <slot name="batch-actions" :selected-rows="selectedRows">
          <el-button
            v-for="action in batchActions"
            :key="action.key"
            :type="action.type"
            :icon="action.icon"
            :disabled="action.disabled"
            :loading="action.loading"
            @click="handleBatchAction(action)"
          >
            {{ action.label }}
          </el-button>
        </slot>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, nextTick, onMounted } from 'vue'
import { ElTable, ElTableColumn, ElButton, ElInput, ElDropdown, ElDropdownMenu, ElDropdownItem, ElCheckbox, ElPagination } from 'element-plus'
import { Search, Refresh, Download, Setting } from '@element-plus/icons-vue'
import type { TableColumnCtx, TableInstance } from 'element-plus'

// 类型定义
interface DataTableColumn {
  prop: string
  label: string
  width?: string | number
  minWidth?: string | number
  fixed?: boolean | 'left' | 'right'
  sortable?: boolean | 'custom'
  sortMethod?: Function
  sortBy?: string | string[] | Function
  sortOrders?: string[]
  resizable?: boolean
  formatter?: Function
  showOverflowTooltip?: boolean
  align?: 'left' | 'center' | 'right'
  headerAlign?: 'left' | 'center' | 'right'
  className?: string
  labelClassName?: string
  filters?: Array<{ text: string; value: any }>
  filterMethod?: Function
  filterMultiple?: boolean
  filterPlacement?: string
  slot?: string
  headerSlot?: string
  component?: any
  componentProps?: Record<string, any>
  visible?: boolean
  children?: DataTableColumn[]
}

interface DataTableAction {
  key: string
  label: string
  type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text'
  size?: 'large' | 'default' | 'small'
  icon?: any
  disabled?: boolean
  loading?: boolean
  show?: (row: any) => boolean
  handler: (row: any, index: number) => void
}

interface DataTableBatchAction {
  key: string
  label: string
  type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text'
  icon?: any
  disabled?: boolean
  loading?: boolean
  handler: (selectedRows: any[]) => void
}

// Props
interface DataTableProps {
  // 数据相关
  data?: any[]
  columns: DataTableColumn[]
  loading?: boolean
  
  // 表格配置
  height?: string | number
  maxHeight?: string | number
  stripe?: boolean
  border?: boolean
  size?: 'large' | 'default' | 'small'
  rowKey?: string | Function
  treeProps?: { children?: string; hasChildren?: string }
  expandRowKeys?: any[]
  defaultSort?: { prop: string; order: 'ascending' | 'descending' }
  highlightCurrentRow?: boolean
  rowClassName?: string | Function
  cellClassName?: string | Function
  spanMethod?: Function
  
  // 功能配置
  showSelection?: boolean
  selectionWidth?: number
  selectionFixed?: boolean | 'left' | 'right'
  selectable?: Function
  
  showIndex?: boolean
  indexLabel?: string
  indexWidth?: number
  indexFixed?: boolean | 'left' | 'right'
  indexMethod?: Function
  
  showExpand?: boolean
  expandWidth?: number
  expandFixed?: boolean | 'left' | 'right'
  
  showActions?: boolean
  actionsLabel?: string
  actionsWidth?: number
  actionsMinWidth?: number
  actionsFixed?: boolean | 'left' | 'right'
  actionsAlign?: 'left' | 'center' | 'right'
  actions?: DataTableAction[]
  
  showBatchActions?: boolean
  batchActions?: DataTableBatchAction[]
  
  // 工具栏配置
  showRefresh?: boolean
  showExport?: boolean
  showSearch?: boolean
  searchPlaceholder?: string
  showColumnSettings?: boolean
  
  // 分页配置
  showPagination?: boolean
  currentPage?: number
  pageSize?: number
  total?: number
  pageSizes?: number[]
  paginationLayout?: string
  paginationBackground?: boolean
  paginationSmall?: boolean
}

const props = withDefaults(defineProps<DataTableProps>(), {
  data: () => [],
  loading: false,
  stripe: true,
  border: true,
  size: 'default',
  highlightCurrentRow: true,
  showSelection: false,
  selectionWidth: 55,
  selectionFixed: 'left',
  showIndex: false,
  indexLabel: '#',
  indexWidth: 55,
  indexFixed: 'left',
  showExpand: false,
  expandWidth: 55,
  expandFixed: 'left',
  showActions: true,
  actionsLabel: '操作',
  actionsWidth: 150,
  actionsFixed: 'right',
  actionsAlign: 'center',
  actions: () => [],
  showBatchActions: true,
  batchActions: () => [],
  showRefresh: true,
  showExport: true,
  showSearch: true,
  searchPlaceholder: '请输入搜索关键词',
  showColumnSettings: true,
  showPagination: true,
  currentPage: 1,
  pageSize: 20,
  total: 0,
  pageSizes: () => [10, 20, 50, 100],
  paginationLayout: 'total, sizes, prev, pager, next, jumper',
  paginationBackground: true,
  paginationSmall: false
})

// Emits
interface DataTableEmits {
  'update:currentPage': [page: number]
  'update:pageSize': [size: number]
  'selection-change': [selection: any[]]
  'current-change': [currentRow: any, oldCurrentRow: any]
  'sort-change': [{ column: TableColumnCtx<any>, prop: string, order: string | null }]
  'expand-change': [row: any, expandedRows: any[]]
  'row-click': [row: any, column: TableColumnCtx<any>, event: Event]
  'row-dblclick': [row: any, column: TableColumnCtx<any>, event: Event]
  'row-contextmenu': [row: any, column: TableColumnCtx<any>, event: Event]
  'cell-click': [row: any, column: TableColumnCtx<any>, cell: HTMLTableCellElement, event: Event]
  'action-click': [action: DataTableAction, row: any, index: number]
  'batch-action': [action: DataTableBatchAction, selectedRows: any[]]
  'refresh': []
  'export': []
  'search': [keyword: string]
}

const emit = defineEmits<DataTableEmits>()

// Refs
const tableRef = ref<TableInstance>()
const searchKeyword = ref('')
const selectedRows = ref<any[]>([])

// Computed
const tableData = computed(() => {
  if (!searchKeyword.value) {
    return props.data
  }
  
  return props.data.filter(row => {
    return Object.values(row).some(value => {
      return String(value).toLowerCase().includes(searchKeyword.value.toLowerCase())
    })
  })
})

const configurableColumns = computed(() => {
  return props.columns.filter(column => !column.children).map(column => ({
    ...column,
    visible: column.visible !== false
  }))
})

const visibleColumns = computed(() => {
  return props.columns.filter(column => {
    if (column.children) {
      return column.children.some(child => child.visible !== false)
    }
    return column.visible !== false
  })
})

// Methods
const handleRefresh = () => {
  emit('refresh')
}

const handleExport = () => {
  emit('export')
}

const handleSearch = (keyword: string) => {
  emit('search', keyword)
}

const toggleColumn = (column: DataTableColumn) => {
  column.visible = !column.visible
}

const handleSelectionChange = (selection: any[]) => {
  selectedRows.value = selection
  emit('selection-change', selection)
}

const handleCurrentChange = (currentRow: any, oldCurrentRow: any) => {
  emit('current-change', currentRow, oldCurrentRow)
}

const handleSortChange = (sortInfo: { column: TableColumnCtx<any>, prop: string, order: string | null }) => {
  emit('sort-change', sortInfo)
}

const handleExpandChange = (row: any, expandedRows: any[]) => {
  emit('expand-change', row, expandedRows)
}

const handleRowClick = (row: any, column: TableColumnCtx<any>, event: Event) => {
  emit('row-click', row, column, event)
}

const handleRowDblclick = (row: any, column: TableColumnCtx<any>, event: Event) => {
  emit('row-dblclick', row, column, event)
}

const handleRowContextmenu = (row: any, column: TableColumnCtx<any>, event: Event) => {
  emit('row-contextmenu', row, column, event)
}

const handleCellClick = (event: Event, scope: any, column: DataTableColumn) => {
  emit('cell-click', scope.row, scope.column, scope.$el, event)
}

const handleSizeChange = (size: number) => {
  emit('update:pageSize', size)
}

const handleCurrentPageChange = (page: number) => {
  emit('update:currentPage', page)
}

const handleActionClick = (action: DataTableAction, scope: any) => {
  emit('action-click', action, scope.row, scope.$index)
  action.handler(scope.row, scope.$index)
}

const handleBatchAction = (action: DataTableBatchAction) => {
  emit('batch-action', action, selectedRows.value)
  action.handler(selectedRows.value)
}

const getRowActions = (row: any) => {
  return props.actions.filter(action => {
    return action.show ? action.show(row) : true
  })
}

const formatCellValue = (row: any, column: DataTableColumn) => {
  if (column.formatter) {
    return column.formatter(row, column, row[column.prop], 0)
  }
  return row[column.prop]
}

const clearSelection = () => {
  tableRef.value?.clearSelection()
}

// 暴露方法
defineExpose({
  tableRef,
  clearSelection,
  toggleRowSelection: (row: any, selected?: boolean) => {
    tableRef.value?.toggleRowSelection(row, selected)
  },
  toggleAllSelection: () => {
    tableRef.value?.toggleAllSelection()
  },
  toggleRowExpansion: (row: any, expanded?: boolean) => {
    tableRef.value?.toggleRowExpansion(row, expanded)
  },
  setCurrentRow: (row: any) => {
    tableRef.value?.setCurrentRow(row)
  },
  clearSort: () => {
    tableRef.value?.clearSort()
  },
  clearFilter: (columnKeys?: string[]) => {
    tableRef.value?.clearFilter(columnKeys)
  },
  doLayout: () => {
    tableRef.value?.doLayout()
  },
  sort: (prop: string, order: string) => {
    tableRef.value?.sort(prop, order)
  }
})
</script>

<style lang="scss" scoped>
.data-table {
  &__toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
    
    &-left {
      display: flex;
      align-items: center;
      gap: 8px;
    }
    
    &-right {
      display: flex;
      align-items: center;
      gap: 8px;
    }
  }
  
  &__pagination {
    display: flex;
    justify-content: flex-end;
    margin-top: 16px;
  }
  
  &__batch-actions {
    position: fixed;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    background: var(--el-bg-color);
    border: 1px solid var(--el-border-color);
    border-radius: 8px;
    padding: 12px 16px;
    box-shadow: var(--el-box-shadow);
    display: flex;
    align-items: center;
    gap: 16px;
    z-index: 1000;
    
    &-info {
      color: var(--el-text-color-regular);
      font-size: 14px;
    }
    
    &-buttons {
      display: flex;
      gap: 8px;
    }
  }
  
  :deep(.data-table__actions) {
    .cell {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 4px;
    }
  }
}
</style>

2.2 智能表单组件

vue
<!-- components/SmartForm/SmartForm.vue -->
<template>
  <el-form
    ref="formRef"
    :model="formData"
    :rules="formRules"
    :label-width="labelWidth"
    :label-position="labelPosition"
    :size="size"
    :disabled="disabled"
    :validate-on-rule-change="validateOnRuleChange"
    :hide-required-asterisk="hideRequiredAsterisk"
    :show-message="showMessage"
    :inline-message="inlineMessage"
    :status-icon="statusIcon"
    @validate="handleValidate"
  >
    <template v-for="field in visibleFields" :key="field.prop">
      <!-- 分组标题 -->
      <div v-if="field.type === 'group'" class="smart-form__group">
        <div class="smart-form__group-title">
          {{ field.label }}
        </div>
        <div class="smart-form__group-description" v-if="field.description">
          {{ field.description }}
        </div>
      </div>
      
      <!-- 分割线 -->
      <el-divider v-else-if="field.type === 'divider'" :content-position="field.contentPosition">
        {{ field.label }}
      </el-divider>
      
      <!-- 表单项 -->
      <el-form-item
        v-else
        :prop="field.prop"
        :label="field.label"
        :label-width="field.labelWidth"
        :required="field.required"
        :rules="getFieldRules(field)"
        :error="fieldErrors[field.prop]"
        :show-message="field.showMessage !== false"
        :inline-message="field.inlineMessage"
        :size="field.size || size"
        :class="getFieldClass(field)"
      >
        <template v-if="field.labelSlot" #label>
          <slot :name="field.labelSlot" :field="field" />
        </template>
        
        <!-- 输入框 -->
        <el-input
          v-if="field.type === 'input'"
          v-model="formData[field.prop]"
          :type="field.inputType || 'text'"
          :placeholder="field.placeholder"
          :clearable="field.clearable !== false"
          :show-password="field.showPassword"
          :show-word-limit="field.showWordLimit"
          :maxlength="field.maxlength"
          :minlength="field.minlength"
          :prefix-icon="field.prefixIcon"
          :suffix-icon="field.suffixIcon"
          :readonly="field.readonly"
          :disabled="field.disabled"
          :size="field.size"
          :resize="field.resize"
          :autosize="field.autosize"
          :autocomplete="field.autocomplete"
          :name="field.name"
          :max="field.max"
          :min="field.min"
          :step="field.step"
          :validate-event="field.validateEvent !== false"
          @input="handleFieldChange(field, $event)"
          @change="handleFieldChange(field, $event)"
          @focus="handleFieldFocus(field, $event)"
          @blur="handleFieldBlur(field, $event)"
        >
          <template v-if="field.prepend" #prepend>
            {{ field.prepend }}
          </template>
          <template v-if="field.append" #append>
            {{ field.append }}
          </template>
        </el-input>
        
        <!-- 文本域 -->
        <el-input
          v-else-if="field.type === 'textarea'"
          v-model="formData[field.prop]"
          type="textarea"
          :placeholder="field.placeholder"
          :rows="field.rows || 3"
          :autosize="field.autosize"
          :maxlength="field.maxlength"
          :minlength="field.minlength"
          :show-word-limit="field.showWordLimit"
          :readonly="field.readonly"
          :disabled="field.disabled"
          :resize="field.resize"
          @input="handleFieldChange(field, $event)"
          @change="handleFieldChange(field, $event)"
          @focus="handleFieldFocus(field, $event)"
          @blur="handleFieldBlur(field, $event)"
        />
        
        <!-- 数字输入框 -->
        <el-input-number
          v-else-if="field.type === 'number'"
          v-model="formData[field.prop]"
          :min="field.min"
          :max="field.max"
          :step="field.step || 1"
          :step-strictly="field.stepStrictly"
          :precision="field.precision"
          :size="field.size"
          :disabled="field.disabled"
          :controls="field.controls !== false"
          :controls-position="field.controlsPosition"
          :placeholder="field.placeholder"
          @change="handleFieldChange(field, $event)"
          @blur="handleFieldBlur(field, $event)"
          @focus="handleFieldFocus(field, $event)"
        />
        
        <!-- 选择器 -->
        <el-select
          v-else-if="field.type === 'select'"
          v-model="formData[field.prop]"
          :multiple="field.multiple"
          :disabled="field.disabled"
          :value-key="field.valueKey"
          :size="field.size"
          :clearable="field.clearable !== false"
          :collapse-tags="field.collapseTags"
          :collapse-tags-tooltip="field.collapseTagsTooltip"
          :multiple-limit="field.multipleLimit"
          :name="field.name"
          :autocomplete="field.autocomplete"
          :placeholder="field.placeholder"
          :filterable="field.filterable"
          :allow-create="field.allowCreate"
          :filter-method="field.filterMethod"
          :remote="field.remote"
          :remote-method="field.remoteMethod"
          :remote-show-suffix="field.remoteShowSuffix"
          :loading="field.loading"
          :loading-text="field.loadingText"
          :no-match-text="field.noMatchText"
          :no-data-text="field.noDataText"
          :popper-class="field.popperClass"
          :reserve-keyword="field.reserveKeyword"
          :default-first-option="field.defaultFirstOption"
          :teleported="field.teleported !== false"
          :persistent="field.persistent"
          :automatic-dropdown="field.automaticDropdown"
          :clear-icon="field.clearIcon"
          :fit-input-width="field.fitInputWidth"
          :suffix-icon="field.suffixIcon"
          :tag-type="field.tagType"
          :validate-event="field.validateEvent !== false"
          @change="handleFieldChange(field, $event)"
          @visible-change="handleSelectVisibleChange(field, $event)"
          @remove-tag="handleSelectRemoveTag(field, $event)"
          @clear="handleSelectClear(field)"
          @blur="handleFieldBlur(field, $event)"
          @focus="handleFieldFocus(field, $event)"
        >
          <el-option
            v-for="option in getSelectOptions(field)"
            :key="option.value"
            :label="option.label"
            :value="option.value"
            :disabled="option.disabled"
          />
        </el-select>
        
        <!-- 级联选择器 -->
        <el-cascader
          v-else-if="field.type === 'cascader'"
          v-model="formData[field.prop]"
          :options="field.options"
          :props="field.cascaderProps"
          :size="field.size"
          :placeholder="field.placeholder"
          :disabled="field.disabled"
          :clearable="field.clearable !== false"
          :show-all-levels="field.showAllLevels !== false"
          :collapse-tags="field.collapseTags"
          :collapse-tags-tooltip="field.collapseTagsTooltip"
          :separator="field.separator"
          :filterable="field.filterable"
          :filter-method="field.filterMethod"
          :debounce="field.debounce"
          :before-filter="field.beforeFilter"
          :popper-class="field.popperClass"
          :teleported="field.teleported !== false"
          :tag-type="field.tagType"
          :validate-event="field.validateEvent !== false"
          @change="handleFieldChange(field, $event)"
          @expand-change="handleCascaderExpandChange(field, $event)"
          @blur="handleFieldBlur(field, $event)"
          @focus="handleFieldFocus(field, $event)"
          @visible-change="handleCascaderVisibleChange(field, $event)"
        />
        
        <!-- 日期选择器 -->
        <el-date-picker
          v-else-if="field.type === 'date'"
          v-model="formData[field.prop]"
          :type="field.dateType || 'date'"
          :placeholder="field.placeholder"
          :start-placeholder="field.startPlaceholder"
          :end-placeholder="field.endPlaceholder"
          :format="field.format"
          :value-format="field.valueFormat"
          :size="field.size"
          :disabled="field.disabled"
          :clearable="field.clearable !== false"
          :readonly="field.readonly"
          :editable="field.editable !== false"
          :disabled-date="field.disabledDate"
          :shortcuts="field.shortcuts"
          :cell-class-name="field.cellClassName"
          :range-separator="field.rangeSeparator"
          :default-value="field.defaultValue"
          :default-time="field.defaultTime"
          :popper-class="field.popperClass"
          :unlink-panels="field.unlinkPanels"
          :prefix-icon="field.prefixIcon"
          :clear-icon="field.clearIcon"
          :validate-event="field.validateEvent !== false"
          @change="handleFieldChange(field, $event)"
          @blur="handleFieldBlur(field, $event)"
          @focus="handleFieldFocus(field, $event)"
          @calendar-change="handleDateCalendarChange(field, $event)"
          @panel-change="handleDatePanelChange(field, $event)"
          @visible-change="handleDateVisibleChange(field, $event)"
        />
        
        <!-- 时间选择器 -->
        <el-time-picker
          v-else-if="field.type === 'time'"
          v-model="formData[field.prop]"
          :is-range="field.isRange"
          :placeholder="field.placeholder"
          :start-placeholder="field.startPlaceholder"
          :end-placeholder="field.endPlaceholder"
          :readonly="field.readonly"
          :disabled="field.disabled"
          :editable="field.editable !== false"
          :clearable="field.clearable !== false"
          :size="field.size"
          :format="field.format"
          :value-format="field.valueFormat"
          :disabled-hours="field.disabledHours"
          :disabled-minutes="field.disabledMinutes"
          :disabled-seconds="field.disabledSeconds"
          :arrow-control="field.arrowControl"
          :popper-class="field.popperClass"
          :range-separator="field.rangeSeparator"
          :prefix-icon="field.prefixIcon"
          :clear-icon="field.clearIcon"
          :default-value="field.defaultValue"
          @change="handleFieldChange(field, $event)"
          @blur="handleFieldBlur(field, $event)"
          @focus="handleFieldFocus(field, $event)"
          @visible-change="handleTimeVisibleChange(field, $event)"
        />
        
        <!-- 开关 -->
        <el-switch
          v-else-if="field.type === 'switch'"
          v-model="formData[field.prop]"
          :disabled="field.disabled"
          :loading="field.loading"
          :size="field.size"
          :width="field.width"
          :inline-prompt="field.inlinePrompt"
          :active-icon="field.activeIcon"
          :inactive-icon="field.inactiveIcon"
          :active-text="field.activeText"
          :inactive-text="field.inactiveText"
          :active-value="field.activeValue"
          :inactive-value="field.inactiveValue"
          :active-color="field.activeColor"
          :inactive-color="field.inactiveColor"
          :border-color="field.borderColor"
          :name="field.name"
          :validate-event="field.validateEvent !== false"
          @change="handleFieldChange(field, $event)"
        />
        
        <!-- 滑块 -->
        <el-slider
          v-else-if="field.type === 'slider'"
          v-model="formData[field.prop]"
          :min="field.min || 0"
          :max="field.max || 100"
          :disabled="field.disabled"
          :step="field.step || 1"
          :show-input="field.showInput"
          :show-input-controls="field.showInputControls !== false"
          :input-size="field.inputSize"
          :show-stops="field.showStops"
          :show-tooltip="field.showTooltip !== false"
          :format-tooltip="field.formatTooltip"
          :range="field.range"
          :vertical="field.vertical"
          :height="field.height"
          :label="field.sliderLabel"
          :debounce="field.debounce"
          :tooltip-class="field.tooltipClass"
          :marks="field.marks"
          @change="handleFieldChange(field, $event)"
          @input="handleSliderInput(field, $event)"
        />
        
        <!-- 评分 -->
        <el-rate
          v-else-if="field.type === 'rate'"
          v-model="formData[field.prop]"
          :max="field.max || 5"
          :disabled="field.disabled"
          :allow-half="field.allowHalf"
          :low-threshold="field.lowThreshold"
          :high-threshold="field.highThreshold"
          :colors="field.colors"
          :void-color="field.voidColor"
          :disabled-void-color="field.disabledVoidColor"
          :icon-classes="field.iconClasses"
          :void-icon-class="field.voidIconClass"
          :disabled-void-icon-class="field.disabledVoidIconClass"
          :show-text="field.showText"
          :show-score="field.showScore"
          :text-color="field.textColor"
          :texts="field.texts"
          :score-template="field.scoreTemplate"
          :size="field.size"
          @change="handleFieldChange(field, $event)"
        />
        
        <!-- 颜色选择器 -->
        <el-color-picker
          v-else-if="field.type === 'color'"
          v-model="formData[field.prop]"
          :disabled="field.disabled"
          :size="field.size"
          :show-alpha="field.showAlpha"
          :color-format="field.colorFormat"
          :popper-class="field.popperClass"
          :predefine="field.predefine"
          :validate-event="field.validateEvent !== false"
          @change="handleFieldChange(field, $event)"
          @active-change="handleColorActiveChange(field, $event)"
        />
        
        <!-- 单选框组 -->
        <el-radio-group
          v-else-if="field.type === 'radio'"
          v-model="formData[field.prop]"
          :size="field.size"
          :disabled="field.disabled"
          :text-color="field.textColor"
          :fill="field.fill"
          :validate-event="field.validateEvent !== false"
          @change="handleFieldChange(field, $event)"
        >
          <template v-if="field.radioType === 'button'">
            <el-radio-button
              v-for="option in field.options"
              :key="option.value"
              :label="option.value"
              :disabled="option.disabled"
            >
              {{ option.label }}
            </el-radio-button>
          </template>
          <template v-else>
            <el-radio
              v-for="option in field.options"
              :key="option.value"
              :label="option.value"
              :disabled="option.disabled"
              :border="field.border"
              :size="field.size"
            >
              {{ option.label }}
            </el-radio>
          </template>
        </el-radio-group>
        
        <!-- 复选框组 -->
        <el-checkbox-group
          v-else-if="field.type === 'checkbox'"
          v-model="formData[field.prop]"
          :size="field.size"
          :disabled="field.disabled"
          :min="field.min"
          :max="field.max"
          :text-color="field.textColor"
          :fill="field.fill"
          :tag="field.tag"
          :validate-event="field.validateEvent !== false"
          @change="handleFieldChange(field, $event)"
        >
          <template v-if="field.checkboxType === 'button'">
            <el-checkbox-button
              v-for="option in field.options"
              :key="option.value"
              :label="option.value"
              :disabled="option.disabled"
            >
              {{ option.label }}
            </el-checkbox-button>
          </template>
          <template v-else>
            <el-checkbox
              v-for="option in field.options"
              :key="option.value"
              :label="option.value"
              :disabled="option.disabled"
              :border="field.border"
              :size="field.size"
              :indeterminate="option.indeterminate"
              :checked="option.checked"
              :true-label="option.trueLabel"
              :false-label="option.falseLabel"
            >
              {{ option.label }}
            </el-checkbox>
          </template>
        </el-checkbox-group>
        
        <!-- 上传 -->
        <el-upload
          v-else-if="field.type === 'upload'"
          :action="field.action"
          :headers="field.headers"
          :method="field.method"
          :multiple="field.multiple"
          :data="field.data"
          :name="field.name"
          :with-credentials="field.withCredentials"
          :show-file-list="field.showFileList !== false"
          :drag="field.drag"
          :accept="field.accept"
          :on-preview="field.onPreview"
          :on-remove="(file, fileList) => handleUploadRemove(field, file, fileList)"
          :on-success="(response, file, fileList) => handleUploadSuccess(field, response, file, fileList)"
          :on-error="(err, file, fileList) => handleUploadError(field, err, file, fileList)"
          :on-progress="(event, file, fileList) => handleUploadProgress(field, event, file, fileList)"
          :on-change="(file, fileList) => handleUploadChange(field, file, fileList)"
          :before-upload="field.beforeUpload"
          :before-remove="field.beforeRemove"
          :list-type="field.listType"
          :auto-upload="field.autoUpload !== false"
          :file-list="formData[field.prop] || []"
          :http-request="field.httpRequest"
          :disabled="field.disabled"
          :limit="field.limit"
          :on-exceed="field.onExceed"
        >
          <template v-if="field.drag">
            <el-icon class="el-icon--upload"><upload-filled /></el-icon>
            <div class="el-upload__text">
              将文件拖到此处,或<em>点击上传</em>
            </div>
          </template>
          <template v-else-if="field.listType === 'picture-card'">
            <el-icon><Plus /></el-icon>
          </template>
          <template v-else>
            <el-button type="primary">点击上传</el-button>
          </template>
          
          <template v-if="field.tip" #tip>
            <div class="el-upload__tip">{{ field.tip }}</div>
          </template>
        </el-upload>
        
        <!-- 自定义组件 -->
        <component
          v-else-if="field.component"
          :is="field.component"
          v-model="formData[field.prop]"
          v-bind="field.componentProps"
          @change="handleFieldChange(field, $event)"
        />
        
        <!-- 插槽 -->
        <slot
          v-else-if="field.slot"
          :name="field.slot"
          :field="field"
          :value="formData[field.prop]"
          :form-data="formData"
          @change="handleFieldChange(field, $event)"
        />
        
        <!-- 帮助文本 -->
        <div v-if="field.help" class="smart-form__help">
          {{ field.help }}
        </div>
      </el-form-item>
    </template>
    
    <!-- 表单操作按钮 -->
    <el-form-item v-if="showActions" class="smart-form__actions">
      <slot name="actions" :form-data="formData" :validate="validate" :reset="resetForm">
        <el-button
          v-if="showSubmit"
          type="primary"
          :loading="submitLoading"
          :disabled="submitDisabled"
          @click="handleSubmit"
        >
          {{ submitText }}
        </el-button>
        
        <el-button
          v-if="showReset"
          :disabled="resetDisabled"
          @click="handleReset"
        >
          {{ resetText }}
        </el-button>
        
        <el-button
          v-if="showCancel"
          :disabled="cancelDisabled"
          @click="handleCancel"
        >
          {{ cancelText }}
        </el-button>
      </slot>
    </el-form-item>
  </el-form>
</template>

<script setup lang="ts">
import { ref, computed, watch, reactive, nextTick } from 'vue'
import { ElForm, ElFormItem, ElInput, ElInputNumber, ElSelect, ElOption, ElCascader, ElDatePicker, ElTimePicker, ElSwitch, ElSlider, ElRate, ElColorPicker, ElRadioGroup, ElRadio, ElRadioButton, ElCheckboxGroup, ElCheckbox, ElCheckboxButton, ElUpload, ElButton, ElDivider, ElIcon } from 'element-plus'
import { Plus, UploadFilled } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'

// 字段类型定义
interface SmartFormField {
  prop: string
  label?: string
  type: string
  required?: boolean
  rules?: any[]
  visible?: boolean | ((formData: any) => boolean)
  disabled?: boolean | ((formData: any) => boolean)
  readonly?: boolean
  placeholder?: string
  help?: string
  labelWidth?: string
  labelSlot?: string
  size?: 'large' | 'default' | 'small'
  showMessage?: boolean
  inlineMessage?: boolean
  slot?: string
  component?: any
  componentProps?: Record<string, any>
  
  // 输入框特有属性
  inputType?: string
  clearable?: boolean
  showPassword?: boolean
  showWordLimit?: boolean
  maxlength?: number
  minlength?: number
  prefixIcon?: any
  suffixIcon?: any
  resize?: 'none' | 'both' | 'horizontal' | 'vertical'
  autosize?: boolean | { minRows?: number; maxRows?: number }
  autocomplete?: string
  name?: string
  max?: number
  min?: number
  step?: number
  validateEvent?: boolean
  prepend?: string
  append?: string
  
  // 文本域特有属性
  rows?: number
  
  // 数字输入框特有属性
  stepStrictly?: boolean
  precision?: number
  controls?: boolean
  controlsPosition?: 'right' | ''
  
  // 选择器特有属性
  options?: Array<{ label: string; value: any; disabled?: boolean }>
  multiple?: boolean
  valueKey?: string
  collapseTags?: boolean
  collapseTagsTooltip?: boolean
  multipleLimit?: number
  filterable?: boolean
  allowCreate?: boolean
  filterMethod?: Function
  remote?: boolean
  remoteMethod?: Function
  remoteShowSuffix?: boolean
  loading?: boolean
  loadingText?: string
  noMatchText?: string
  noDataText?: string
  popperClass?: string
  reserveKeyword?: boolean
  defaultFirstOption?: boolean
  teleported?: boolean
  persistent?: boolean
  automaticDropdown?: boolean
  clearIcon?: any
  fitInputWidth?: boolean

Element Plus Study Guide