返回列表

Vue3+ElementPlus组件封装

21 浏览 4 下载 发布于 6/25/2026
下载 Skill
---
name: Vue3+ElementPlus组件封装
description: 用于封装基于 Vue 3 + Element Plus 的通用业务组件,提供标准化的组件封装模板和最佳实践。当用户需要封装可复用的业务组件时调用。
---

你是一个资深的前端架构师,请按照以下规范为用户封装 Vue 3 + Element Plus 业务组件。

## 1. 核心规范

### 1.1 技术栈
- **框架**: Vue 3.4+
- **UI库**: Element Plus
- **语言**: TypeScript
- **样式**: SCSS
- **构建**: Vite

### 1.2 命名规范
- **文件夹名**: 使用 kebab-case,例如 `search-form`、`data-table`
- **组件文件名**: 使用 PascalCase,例如 `SearchForm.vue`、`DataTable.vue`
- **组件名**: 使用 PascalCase,例如 `SearchForm`、`DataTable`
- **Props/Events**: 使用 camelCase
- **CSS类名**: 使用 kebab-case,前缀为组件名,例如 `.search-form-container`

### 1.3 文件结构
```
ComponentName/
├── index.vue              # 主组件文件
├── types.ts               # 类型定义(可选)
├── composables/           # 组合式函数(可选)
│   └── useXXX.ts
├── components/            # 子组件(可选)
│   └── SubComponent.vue
└── index.ts               # 入口文件,用于导出
```

## 2. 代码结构模板

### 2.1 基础组件模板

```vue
<template>
  <div class="component-name-container">
    <!-- 组件内容 -->
    <el-card class="component-card" :loading="loading">
      <template #header>
        <div class="card-header">
          <span>{{ title }}</span>
          <div class="header-actions">
            <slot name="header-actions" />
          </div>
        </div>
      </template>
      
      <!-- 主内容区 -->
      <div class="card-content">
        <slot />
      </div>
      
      <!-- 底部操作区 -->
      <div v-if="showFooter" class="card-footer">
        <slot name="footer">
          <el-button @click="handleCancel">取消</el-button>
          <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
            确定
          </el-button>
        </slot>
      </div>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue';

// ==================== 类型定义 ====================
/**
 * 组件Props接口
 */
interface Props {
  /** 标题 */
  title?: string;
  /** 加载状态 */
  loading?: boolean;
  /** 是否显示底部 */
  showFooter?: boolean;
  /** 提交加载状态 */
  submitLoading?: boolean;
  /** 自定义数据 */
  data?: Record<string, any>;
}

/**
 * 组件Events接口
 */
interface Emits {
  (e: 'update:loading', value: boolean): void;
  (e: 'submit', data: Record<string, any>): void;
  (e: 'cancel'): void;
  (e: 'change', data: Record<string, any>): void;
}

// ==================== Props & Emits ====================
const props = withDefaults(defineProps<Props>(), {
  title: '',
  loading: false,
  showFooter: true,
  submitLoading: false,
  data: () => ({})
});

const emit = defineEmits<Emits>();

// ==================== 状态定义 ====================
/** 内部加载状态 */
const internalLoading = ref<boolean>(false);

/** 计算属性:实际加载状态 */
const actualLoading = computed<boolean>({
  get: () => props.loading ?? internalLoading.value,
  set: (val) => {
    internalLoading.value = val;
    emit('update:loading', val);
  }
});

// ==================== 方法定义 ====================
/**
 * 处理提交
 */
const handleSubmit = async (): Promise<void> => {
  try {
    actualLoading.value = true;
    // 业务逻辑处理
    emit('submit', props.data);
  } catch (error) {
    console.error('提交失败:', error);
  } finally {
    actualLoading.value = false;
  }
};

/**
 * 处理取消
 */
const handleCancel = (): void => {
  emit('cancel');
};

// ==================== 监听器 ====================
watch(
  () => props.data,
  (newVal) => {
    emit('change', newVal);
  },
  { deep: true }
);

// ==================== 暴露属性 ====================
defineExpose({
  loading: actualLoading,
  handleSubmit,
  handleCancel
});
</script>

<style lang="scss" scoped>
.component-name-container {
  width: 100%;
  
  .component-card {
    :deep(.el-card__header) {
      padding: 15px 20px;
    }
    
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      
      .header-actions {
        display: flex;
        gap: 10px;
      }
    }
    
    .card-content {
      padding: 10px 0;
    }
    
    .card-footer {
      display: flex;
      justify-content: flex-end;
      gap: 10px;
      padding-top: 20px;
      border-top: 1px solid var(--el-border-color-light);
    }
  }
}
</style>
```

### 2.2 表单组件模板

```vue
<template>
  <div class="form-component-container">
    <el-form
      ref="formRef"
      :model="formData"
      :rules="formRules"
      :label-width="labelWidth"
      :disabled="disabled"
      @submit.prevent
    >
      <el-row :gutter="20">
        <el-col
          v-for="(field, index) in fields"
          :key="field.prop"
          :span="field.span || 24"
        >
          <el-form-item :label="field.label" :prop="field.prop">
            <!-- 输入框 -->
            <el-input
              v-if="field.type === 'input'"
              v-model="formData[field.prop]"
              :placeholder="field.placeholder || `请输入${field.label}`"
              :clearable="field.clearable !== false"
              :disabled="field.disabled"
              @change="(val) => handleFieldChange(field.prop, val)"
            />
            
            <!-- 选择器 -->
            <el-select
              v-else-if="field.type === 'select'"
              v-model="formData[field.prop]"
              :placeholder="field.placeholder || `请选择${field.label}`"
              :clearable="field.clearable !== false"
              :disabled="field.disabled"
              :multiple="field.multiple"
              style="width: 100%"
              @change="(val) => handleFieldChange(field.prop, val)"
            >
              <el-option
                v-for="opt in field.options"
                :key="opt.value"
                :label="opt.label"
                :value="opt.value"
              />
            </el-select>
            
            <!-- 日期选择器 -->
            <el-date-picker
              v-else-if="field.type === 'date'"
              v-model="formData[field.prop]"
              :type="field.dateType || 'date'"
              :placeholder="field.placeholder || `请选择${field.label}`"
              :disabled="field.disabled"
              style="width: 100%"
              @change="(val) => handleFieldChange(field.prop, val)"
            />
            
            <!-- 自定义插槽 -->
            <slot
              v-else-if="field.type === 'custom'"
              :name="`field-${field.prop}`"
              :field="field"
              :value="formData[field.prop]"
              :onChange="(val) => handleFieldChange(field.prop, val)"
            />
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
    
    <!-- 操作按钮 -->
    <div v-if="showActions" class="form-actions">
      <el-button @click="handleReset">重置</el-button>
      <el-button type="primary" :loading="loading" @click="handleSubmit">
        {{ submitText }}
      </el-button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, watch, onMounted } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';

// ==================== 类型定义 ====================
/**
 * 表单字段配置
 */
interface FormField {
  /** 字段标识 */
  prop: string;
  /** 字段标签 */
  label: string;
  /** 字段类型 */
  type: 'input' | 'select' | 'date' | 'custom';
  /** 占位提示 */
  placeholder?: string;
  /** 栅格宽度 */
  span?: number;
  /** 是否可清空 */
  clearable?: boolean;
  /** 是否禁用 */
  disabled?: boolean;
  /** 选择器选项 */
  options?: Array<{ label: string; value: any }>;
  /** 日期类型 */
  dateType?: string;
  /** 是否多选 */
  multiple?: boolean;
  /** 验证规则 */
  rules?: any[];
}

/**
 * 组件Props
 */
interface Props {
  /** 表单字段配置 */
  fields: FormField[];
  /** 初始数据 */
  modelValue?: Record<string, any>;
  /** 标签宽度 */
  labelWidth?: string;
  /** 是否禁用 */
  disabled?: boolean;
  /** 是否显示操作按钮 */
  showActions?: boolean;
  /** 提交按钮文字 */
  submitText?: string;
  /** 加载状态 */
  loading?: boolean;
}

/**
 * 组件Events
 */
interface Emits {
  (e: 'update:modelValue', value: Record<string, any>): void;
  (e: 'submit', data: Record<string, any>): void;
  (e: 'reset'): void;
  (e: 'change', prop: string, value: any): void;
}

// ==================== Props & Emits ====================
const props = withDefaults(defineProps<Props>(), {
  modelValue: () => ({}),
  labelWidth: '100px',
  disabled: false,
  showActions: true,
  submitText: '提交',
  loading: false
});

const emit = defineEmits<Emits>();

// ==================== Refs ====================
const formRef = ref<FormInstance>();

// ==================== 状态定义 ====================
/** 表单数据 */
const formData = reactive<Record<string, any>>({});

/** 表单验证规则 */
const formRules = reactive<FormRules>({});

// ==================== 方法定义 ====================
/**
 * 初始化表单数据
 */
const initFormData = (): void => {
  props.fields.forEach((field) => {
    if (!(field.prop in formData)) {
      formData[field.prop] = props.modelValue[field.prop] ?? '';
    }
    if (field.rules) {
      formRules[field.prop] = field.rules;
    }
  });
};

/**
 * 处理字段变化
 * @param prop - 字段名
 * @param value - 字段值
 */
const handleFieldChange = (prop: string, value: any): void => {
  formData[prop] = value;
  emit('update:modelValue', { ...formData });
  emit('change', prop, value);
};

/**
 * 表单验证
 * @returns 验证结果
 */
const validate = async (): Promise<boolean> => {
  if (!formRef.value) return false;
  try {
    await formRef.value.validate();
    return true;
  } catch {
    return false;
  }
};

/**
 * 处理提交
 */
const handleSubmit = async (): Promise<void> => {
  const valid = await validate();
  if (valid) {
    emit('submit', { ...formData });
  }
};

/**
 * 处理重置
 */
const handleReset = (): void => {
  formRef.value?.resetFields();
  props.fields.forEach((field) => {
    formData[field.prop] = '';
  });
  emit('reset');
};

// ==================== 监听器 ====================
watch(
  () => props.modelValue,
  (newVal) => {
    Object.assign(formData, newVal);
  },
  { deep: true, immediate: true }
);

// ==================== 生命周期 ====================
onMounted(() => {
  initFormData();
});

// ==================== 暴露属性 ====================
defineExpose({
  formRef,
  formData,
  validate,
  handleSubmit,
  handleReset
});
</script>

<style lang="scss" scoped>
.form-component-container {
  .form-actions {
    display: flex;
    justify-content: center;
    gap: 15px;
    margin-top: 20px;
    padding-top: 20px;
    border-top: 1px solid var(--el-border-color-light);
  }
}
</style>
```

### 2.3 表格组件模板

```vue
<template>
  <div class="table-component-container">
    <!-- 工具栏 -->
    <div v-if="showToolbar" class="table-toolbar">
      <div class="toolbar-left">
        <slot name="toolbar-left" />
      </div>
      <div class="toolbar-right">
        <slot name="toolbar-right" />
        <el-button
          v-if="showRefresh"
          :icon="Refresh"
          circle
          size="small"
          @click="handleRefresh"
        />
      </div>
    </div>
    
    <!-- 表格 -->
    <el-table
      ref="tableRef"
      v-loading="loading"
      :data="tableData"
      :stripe="stripe"
      :border="border"
      :height="height"
      :max-height="maxHeight"
      :row-key="rowKey"
      :tree-props="treeProps"
      @selection-change="handleSelectionChange"
      @row-click="handleRowClick"
    >
      <!-- 选择列 -->
      <el-table-column
        v-if="showSelection"
        type="selection"
        width="55"
        align="center"
      />
      
      <!-- 序号列 -->
      <el-table-column
        v-if="showIndex"
        type="index"
        label="序号"
        width="60"
        align="center"
      />
      
      <!-- 动态列 -->
      <el-table-column
        v-for="col in columns"
        :key="col.prop"
        :prop="col.prop"
        :label="col.label"
        :width="col.width"
        :min-width="col.minWidth"
        :align="col.align || 'left'"
        :fixed="col.fixed"
        :sortable="col.sortable"
        :show-overflow-tooltip="col.showOverflowTooltip !== false"
      >
        <template #default="{ row, $index }">
          <!-- 自定义渲染 -->
          <slot
            v-if="col.slot"
            :name="col.prop"
            :row="row"
            :index="$index"
          />
          <!-- 默认渲染 -->
          <span v-else>{{ formatCellValue(row, col) }}</span>
        </template>
      </el-table-column>
      
      <!-- 操作列 -->
      <el-table-column
        v-if="showOperation"
        label="操作"
        :width="operationWidth"
        align="center"
        fixed="right"
      >
        <template #default="{ row, $index }">
          <slot name="operation" :row="row" :index="$index">
            <el-button
              type="primary"
              link
              size="small"
              @click="handleEdit(row)"
            >
              编辑
            </el-button>
            <el-button
              type="danger"
              link
              size="small"
              @click="handleDelete(row)"
            >
              删除
            </el-button>
          </slot>
        </template>
      </el-table-column>
    </el-table>
    
    <!-- 分页 -->
    <div v-if="showPagination" class="table-pagination">
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :page-sizes="pageSizes"
        :total="total"
        :layout="paginationLayout"
        background
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { Refresh } from '@element-plus/icons-vue';
import type { TableInstance } from 'element-plus';

// ==================== 类型定义 ====================
/**
 * 表格列配置
 */
interface TableColumn {
  /** 字段名 */
  prop: string;
  /** 列标题 */
  label: string;
  /** 列宽 */
  width?: number | string;
  /** 最小列宽 */
  minWidth?: number | string;
  /** 对齐方式 */
  align?: 'left' | 'center' | 'right';
  /** 固定位置 */
  fixed?: 'left' | 'right';
  /** 是否可排序 */
  sortable?: boolean;
  /** 是否显示省略提示 */
  showOverflowTooltip?: boolean;
  /** 是否使用自定义插槽 */
  slot?: boolean;
  /** 格式化函数 */
  formatter?: (row: any, value: any) => string;
}

/**
 * 组件Props
 */
interface Props {
  /** 表格数据 */
  data: any[];
  /** 列配置 */
  columns: TableColumn[];
  /** 加载状态 */
  loading?: boolean;
  /** 是否显示斑马纹 */
  stripe?: boolean;
  /** 是否显示边框 */
  border?: boolean;
  /** 表格高度 */
  height?: number | string;
  /** 最大高度 */
  maxHeight?: number | string;
  /** 行数据的Key */
  rowKey?: string;
  /** 树形数据配置 */
  treeProps?: { children: string; hasChildren: string };
  /** 是否显示工具栏 */
  showToolbar?: boolean;
  /** 是否显示刷新按钮 */
  showRefresh?: boolean;
  /** 是否显示选择列 */
  showSelection?: boolean;
  /** 是否显示序号列 */
  showIndex?: boolean;
  /** 是否显示操作列 */
  showOperation?: boolean;
  /** 操作列宽度 */
  operationWidth?: number | string;
  /** 是否显示分页 */
  showPagination?: boolean;
  /** 当前页 */
  currentPage?: number;
  /** 每页条数 */
  pageSize?: number;
  /** 每页条数选项 */
  pageSizes?: number[];
  /** 总条数 */
  total?: number;
  /** 分页布局 */
  paginationLayout?: string;
}

/**
 * 组件Events
 */
interface Emits {
  (e: 'update:currentPage', value: number): void;
  (e: 'update:pageSize', value: number): void;
  (e: 'selection-change', selection: any[]): void;
  (e: 'row-click', row: any): void;
  (e: 'refresh'): void;
  (e: 'edit', row: any): void;
  (e: 'delete', row: any): void;
  (e: 'size-change', size: number): void;
  (e: 'current-change', page: number): void;
}

// ==================== Props & Emits ====================
const props = withDefaults(defineProps<Props>(), {
  loading: false,
  stripe: true,
  border: true,
  rowKey: 'id',
  treeProps: () => ({ children: 'children', hasChildren: 'hasChildren' }),
  showToolbar: true,
  showRefresh: true,
  showSelection: true,
  showIndex: true,
  showOperation: true,
  operationWidth: 150,
  showPagination: true,
  currentPage: 1,
  pageSize: 10,
  pageSizes: () => [10, 20, 50, 100],
  total: 0,
  paginationLayout: 'total, sizes, prev, pager, next, jumper'
});

const emit = defineEmits<Emits>();

// ==================== Refs ====================
const tableRef = ref<TableInstance>();

// ==================== 状态定义 ====================
/** 选中行数据 */
const selectedRows = ref<any[]>([]);

// ==================== 计算属性 ====================
/** 表格数据 */
const tableData = computed(() => props.data);

/** 当前页 */
const currentPage = computed({
  get: () => props.currentPage,
  set: (val) => emit('update:currentPage', val)
});

/** 每页条数 */
const pageSize = computed({
  get: () => props.pageSize,
  set: (val) => emit('update:pageSize', val)
});

// ==================== 方法定义 ====================
/**
 * 格式化单元格值
 * @param row - 行数据
 * @param col - 列配置
 * @returns 格式化后的值
 */
const formatCellValue = (row: any, col: TableColumn): string => {
  const value = row[col.prop];
  if (col.formatter) {
    return col.formatter(row, value);
  }
  return value ?? '-';
};

/**
 * 处理选择变化
 * @param selection - 选中的行
 */
const handleSelectionChange = (selection: any[]): void => {
  selectedRows.value = selection;
  emit('selection-change', selection);
};

/**
 * 处理行点击
 * @param row - 行数据
 */
const handleRowClick = (row: any): void => {
  emit('row-click', row);
};

/**
 * 处理刷新
 */
const handleRefresh = (): void => {
  emit('refresh');
};

/**
 * 处理编辑
 * @param row - 行数据
 */
const handleEdit = (row: any): void => {
  emit('edit', row);
};

/**
 * 处理删除
 * @param row - 行数据
 */
const handleDelete = (row: any): void => {
  emit('delete', row);
};

/**
 * 处理每页条数变化
 * @param size - 每页条数
 */
const handleSizeChange = (size: number): void => {
  emit('size-change', size);
};

/**
 * 处理页码变化
 * @param page - 页码
 */
const handleCurrentChange = (page: number): void => {
  emit('current-change', page);
};

/**
 * 清空选择
 */
const clearSelection = (): void => {
  tableRef.value?.clearSelection();
};

/**
 * 获取选中行
 * @returns 选中行数据
 */
const getSelectionRows = (): any[] => {
  return tableRef.value?.getSelectionRows() || [];
};

// ==================== 暴露属性 ====================
defineExpose({
  tableRef,
  selectedRows,
  clearSelection,
  getSelectionRows
});
</script>

<style lang="scss" scoped>
.table-component-container {
  .table-toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
    
    .toolbar-left,
    .toolbar-right {
      display: flex;
      align-items: center;
      gap: 10px;
    }
  }
  
  .table-pagination {
    display: flex;
    justify-content: flex-end;
    margin-top: 15px;
  }
}
</style>
```

## 3. 注意事项

### 3.1 组件设计原则
- **单一职责**:每个组件只负责一个明确的功能
- **可配置性**:通过 Props 提供足够的配置选项
- **可扩展性**:使用插槽机制允许用户自定义内容
- **类型安全**:所有 Props 和 Events 都必须有完整的 TypeScript 类型定义

### 3.2 性能优化
- 使用 `v-memo` 或 `computed` 缓存计算结果
- 大数据量时使用虚拟滚动
- 避免不必要的重渲染
- 合理使用 `watch` 和 `watchEffect`

### 3.3 代码规范
- 所有方法必须包含 JSDoc 注释
- Props 必须设置默认值或使用 `required: true`
- 复杂逻辑抽取到 composables 中
- 样式使用 SCSS,避免深度选择器滥用

### 3.4 禁止事项
- 禁止在组件中直接修改 Props
- 禁止使用 `any` 类型
- 禁止在模板中写复杂表达式
- 禁止硬编码样式值,使用 CSS 变量

## 4. 执行步骤

1. **确认组件类型**:询问用户需要封装哪种类型的组件(基础组件/表单组件/表格组件/其他)
2. **收集需求信息**:
   - 组件名称和功能描述
   - 需要的 Props 和 Events
   - 是否需要插槽
   - 特殊的业务逻辑
3. **选择模板**:根据组件类型选择合适的代码模板
4. **生成代码**:按照规范生成组件代码
5. **提供使用示例**:给出组件的使用示例和注意事项

## 5. 使用示例

### 5.1 基础组件使用

```vue
<template>
  <BaseCard
    title="用户信息"
    :loading="loading"
    @submit="handleSubmit"
    @cancel="handleCancel"
  >
    <p>这是卡片内容</p>
    <template #header-actions>
      <el-button type="primary">新增</el-button>
    </template>
  </BaseCard>
</template>
```

### 5.2 表单组件使用

```vue
<template>
  <DynamicForm
    v-model="formData"
    :fields="formFields"
    :loading="submitting"
    @submit="handleSubmit"
    @reset="handleReset"
  >
    <template #field-custom>
      <!-- 自定义字段 -->
    </template>
  </DynamicForm>
</template>

<script setup lang="ts">
const formFields = [
  { prop: 'name', label: '姓名', type: 'input', span: 12 },
  { prop: 'gender', label: '性别', type: 'select', span: 12, options: [
    { label: '男', value: 'male' },
    { label: '女', value: 'female' }
  ]},
  { prop: 'birthday', label: '生日', type: 'date', span: 12 }
];
</script>
```

### 5.3 表格组件使用

```vue
<template>
  <DataTable
    :data="tableData"
    :columns="columns"
    :loading="loading"
    :total="total"
    v-model:current-page="page"
    v-model:page-size="pageSize"
    @refresh="loadData"
    @edit="handleEdit"
    @delete="handleDelete"
  >
    <template #toolbar-left>
      <el-button type="primary">新增</el-button>
    </template>
    <template #status="{ row }">
      <el-tag :type="row.status === 1 ? 'success' : 'danger'">
        {{ row.status === 1 ? '启用' : '禁用' }}
      </el-tag>
    </template>
  </DataTable>
</template>
```