---
name: "element-plus-patterns"
version: "1.0.0"
origin: "captured"
generation: 0
parent_skill_ids: []
status: "stable"
description: "IMEX EOMS Element Plus 高频组件组合模式库。固化业务页面中表单分组、选择器、弹层、状态显示、异步表格、复杂校验、列渲染等场景的推荐组合与反模式，作为 Copilot Frontend Developer 的代码生成参考。"
trigger_phases: ["implementation"]
applicable_agents: ["Copilot Frontend Developer", "Copilot Implementation"]
priority: 20
---

# Element Plus Patterns — 高频组件组合模式库

> **适用范围**: IMEX EOMS 所有 Vue 3 + Element Plus 业务页面。  
> **维护者**: `Copilot Frontend Developer` + `Copilot UI Designer`。  
> **关联**: `industrial-page-standard/SKILL.md`（页面骨架）/ `anti-patterns.md` §5（Element Plus 误用）/ `modern-css-2026/SKILL.md`（共存矩阵）。  
> **使用方式**: 生成新页面或新交互时，先查本文件是否有现成模式；有则直接套用，无则在本文件追加新章节。

---

## 1. 表单分组（Form Grouping）

### 1.1 推荐模式: Section Card + el-row/el-col

```vue
<template>
  <el-form
    ref="formRef"
    :model="formData"
    :rules="formRules"
    label-position="top"
    class="im-form-field--underline"
  >
    <!-- Section 1: 核心识别信息 -->
    <div class="im-section-card">
      <div class="im-section-head">
        <el-icon><Document /></el-icon>
        <span>{{ t('module.entity.section.identity') }}</span>
        <el-badge :value="3" type="warning" v-if="hasIdentityChange" />
      </div>
      <div class="im-section-body">
        <el-row :gutter="16">
          <el-col :span="12">
            <el-form-item :label="t('module.entity.code')" prop="code" required>
              <el-input v-model="formData.code" :placeholder="t('common.autoGenerate')" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item :label="t('module.entity.name')" prop="name" required>
              <el-input v-model="formData.name" maxlength="100" show-word-limit />
            </el-form-item>
          </el-col>
        </el-row>
      </div>
    </div>

    <!-- Section 2: 业务主属性 -->
    <div class="im-section-card">
      <div class="im-section-head">
        <el-icon><Setting /></el-icon>
        <span>{{ t('module.entity.section.business') }}</span>
      </div>
      <div class="im-section-body">
        <!-- 字段 -->
      </div>
    </div>

    <!-- Section 3: 继承字段（只读） -->
    <div class="im-section-card im-section-card--inherited">
      <div class="im-section-head">
        <el-icon><Link /></el-icon>
        <span>{{ t('module.entity.section.inherited') }}</span>
        <el-tag size="small" type="info">{{ t('common.fromMDM') }}</el-tag>
      </div>
      <div class="im-section-body">
        <!-- disabled 字段 -->
      </div>
    </div>
  </el-form>
</template>
```

### 1.2 反模式

- ❌ 单一长表单无分段（≥ 5 个业务字段强制分组）
- ❌ 用 `el-collapse` 折叠所有 section（用户必须手动展开才能看见，认知负担重）
- ❌ Section 标题用 `<h3>` 替代 `.im-section-head`（失去图标 + Badge 能力）

### 1.3 Section 图标与阅读态模式

**推荐模式**:
- 标准业务 section 图标通过 `src/config/enterpriseSectionIcons.js` 统一解析，避免在每个页面内重复维护 `Document / User / Coin / Box` 等图标选择
- 页面中的 `sectionNavItems` 应至少包含 `key`、`label`、`icon` 或 `iconTheme`，使右侧导航与 section 标题保持一致
- 阅读态字段继续保留 `el-form-item` 容器与 `.im-form-field--underline` 样式，仅切换为只读视觉，不改布局骨架

**推荐示例**:

```vue
<script setup>
import { computed } from 'vue';
import { decorateEnterpriseSectionNavItems } from '@/config/enterpriseSectionIcons';

const sectionNavItems = computed(() =>
  decorateEnterpriseSectionNavItems(
    [
      { key: 'basic', label: t('module.form.sections.basic') },
      { key: 'contact', label: t('module.form.sections.contact') },
      { key: 'finance', label: t('module.form.sections.finance') },
    ],
    'contract'
  )
);
</script>

<template>
  <button v-for="item in sectionNavItems" :key="item.key" type="button" class="enterprise-form__nav-item">
    <el-icon><component :is="item.icon" /></el-icon>
    <span>{{ item.label }}</span>
  </button>
</template>
```

**反模式**:
- ❌ 每个页面都手写一套 section 图标映射，导致同主题图标漂移
- ❌ 阅读态直接改成纯文本 `div`，导致表单标签、底线、栅格全部丢失
- ❌ 标准 section 每页单独 import 图标，而不是复用全局注册名 + registry


---

## 2. 选择器分发（FK Selector）

### 2.1 决策树

```
FK 字段类型？
├─ lookup（查找型，候选项 > 50）→ ImLookupSelector
├─ select（枚举/字典，候选项 ≤ 50）→ el-select + dicData
├─ cascader（层级关系，如部门树）→ el-cascader
└─ tree（多选树形）→ el-tree-select
```

### 2.2 ImLookupSelector（lookup）

```vue
<el-form-item :label="t('module.entity.enterpriseId')" prop="enterpriseId" required>
  <ImLookupSelector
    v-model="formData.enterpriseId"
    v-model:label="formData.enterpriseIdName"
    :selector-component="EnterpriseSelector"
    :placeholder="t('common.selectPlaceholder')"
    :disabled="mode === 'edit' && enterpriseLocked"
  />
</el-form-item>
```

**规范**:
- 必须同时绑定 `v-model:label="<fieldName>Name"`，submit 时一并提交
- 列表/详情显示 `<fieldName>Name`，不显示原始 ID
- selectorComponent 来自 `src/views/<module>/components/<Entity>Selector.vue`，遵循"业务选择器组件规范"（industrial-page-standard §11）

### 2.3 el-select + dicData（字典/枚举）

```vue
<el-form-item :label="t('module.entity.status')" prop="status" required>
  <el-select v-model="formData.status" :placeholder="t('common.selectPlaceholder')" clearable>
    <el-option
      v-for="item in statusDic"
      :key="item.value"
      :label="item.label"
      :value="item.value"
    >
      <span>{{ item.label }}</span>
      <el-tag size="small" :type="item.tagType" class="ml-2">{{ item.value }}</el-tag>
    </el-option>
  </el-select>
</el-form-item>

<script setup>
import { computed } from 'vue';
const statusDic = computed(() => createStatusDic(t));  // 复用 option/<entity>.js 的字典工厂
</script>
```

**规范**:
- 字典用 `createXxxDic(t)` 工厂函数生成（i18n 友好）
- 大数据量字典（> 200 项）改用 `el-select-v2`（虚拟滚动）
- 远程字典用 `:remote="true"` + `:remote-method`，加 debounce

### 2.4 el-cascader（层级）

```vue
<el-form-item :label="t('module.entity.orgId')" prop="orgId" required>
  <el-cascader
    v-model="formData.orgId"
    :options="orgTree"
    :props="{ checkStrictly: false, value: 'id', label: 'name', children: 'children', emitPath: false }"
    :placeholder="t('common.selectPlaceholder')"
    clearable
    filterable
  />
</el-form-item>
```

### 2.5 反模式

- ❌ FK 字段用 `el-input` 让用户输 ID
- ❌ 字典硬编码在模板中（i18n 失效、复用困难）
- ❌ 大字典不用 `el-select-v2`（卡顿）
- ❌ 列表显示原始 ID，不显示 label（用户无法理解）

---

## 3. 弹层选择（Drawer / Dialog）

### 3.1 决策矩阵

| 场景 | 推荐 | 宽度 |
|---|---|---|
| 新建/编辑表单（≤ 3 sections） | `el-drawer` | 60% |
| 新建/编辑表单（≥ 3 sections） | `el-drawer` | 70% |
| 详情 Paper（A4 档案） | `el-drawer` | 70% |
| 业务对象选择器 | `el-dialog` | 800px |
| 二次确认 | `ElMessageBox.confirm` | 自动 |
| 复杂操作（批量 / 工作流操作） | `el-drawer` | 50% |
| 错误详情 / 帮助文档内嵌 | `el-dialog` | 600px |

### 3.2 标准 Drawer 模板

```vue
<el-drawer
  v-model="drawerOpen"
  :title="title"
  :size="'70%'"
  :destroy-on-close="true"
  :close-on-click-modal="false"
  :close-on-press-escape="true"
  direction="rtl"
>
  <template #header>
    <div class="im-drawer-header">
      <div class="im-drawer-header__title">
        <el-icon><Edit /></el-icon>
        <span>{{ title }}</span>
        <el-tag :type="modeTag.type" size="small">{{ modeTag.label }}</el-tag>
      </div>
    </div>
  </template>

  <div class="im-drawer-body">
    <FormComponent
      ref="formRef"
      :mode="mode"
      :target-id="targetId"
      @success="handleSuccess"
    />
  </div>

  <template #footer>
    <div class="im-drawer-footer">
      <el-button @click="drawerOpen = false">{{ t('common.cancel') }}</el-button>
      <el-button type="primary" :loading="submitting" @click="submit">
        {{ t('common.save') }}
      </el-button>
    </div>
  </template>
</el-drawer>
```

### 3.3 反模式

- ❌ `el-dialog` 装表单（宽度受限，长表单滚动体验差）
- ❌ 不开 `destroy-on-close`（状态残留导致下次打开数据错乱）
- ❌ 同一组件内多个 Drawer 共用一个 boolean ref（互相冲突）
- ❌ Drawer 内嵌 Drawer（用户迷失）

---

## 4. 状态显示（Status Badge）

### 4.1 推荐模式

```vue
<template #status="{ row }">
  <el-tag :type="getStatusTagType(row.status)" :class="`im-badge im-badge--${getStatusSemantic(row.status)}`">
    <el-icon class="mr-1"><component :is="getStatusIcon(row.status)" /></el-icon>
    {{ getStatusLabel(row.status) }}
  </el-tag>
</template>

<script setup>
const getStatusSemantic = (status) => {
  const map = { ACTIVE: 'success', PENDING: 'warning', DISABLED: 'danger', DRAFT: 'default' };
  return map[status] || 'default';
};
const getStatusTagType = (status) => {
  const map = { ACTIVE: 'success', PENDING: 'warning', DISABLED: 'danger', DRAFT: 'info' };
  return map[status] || 'info';
};
const getStatusIcon = (status) => {
  const map = { ACTIVE: 'CircleCheck', PENDING: 'Clock', DISABLED: 'CircleClose', DRAFT: 'EditPen' };
  return map[status] || 'QuestionFilled';
};
const getStatusLabel = (status) => {
  return statusDic.value.find(d => d.value === status)?.label ?? status;
};
</script>
```

### 4.2 规范

- 状态色映射到五种语义桶（success / warning / danger / primary / default）
- 必须 + 图标，不得仅依赖颜色
- 必须字典翻译，不得显示原始 enum
- 不发明新色

### 4.3 反模式

- ❌ `<span>{{ row.status }}</span>` 直接显示 ENUM
- ❌ 用 `style="color: red"` 自创颜色
- ❌ 不传 `type` 让 el-tag 默认灰色（视觉无差异）

---

## 5. 异步表格加载（Async Table）

### 5.1 推荐模式

```javascript
const loading = ref(false);
const data = ref([]);
const page = ref({ currentPage: 1, pageSize: 15, total: 0 });
const search = ref({});

const onLoad = async (pageParam = page.value) => {
  loading.value = true;
  try {
    const params = normalizeSearch(search.value);
    const res = await getList(pageParam.currentPage, pageParam.pageSize, params);
    data.value = res.data.data.records;
    page.value.total = res.data.data.total;
  } catch (err) {
    ElMessage.error(err?.message || t('common.loadFailed'));
  } finally {
    loading.value = false;
  }
};
```

### 5.2 多接口聚合（Inspector — 带竞态保护）

```javascript
const inspectorRequestToken = ref(0);
const inspectorLoading = ref(false);

const loadInspector = async (row) => {
  const token = ++inspectorRequestToken.value;
  inspectorLoading.value = true;
  try {
    const [detailRes, timelineRes, statsRes] = await Promise.all([
      getDetail(row.id),
      getTimeline(row.id),
      getStats(row.id),
    ]);
    if (token !== inspectorRequestToken.value) return; // 过期请求丢弃
    inspectorData.value = detailRes.data.data;
    inspectorTimeline.value = timelineRes.data.data;
    inspectorStats.value = statsRes.data.data;
  } finally {
    if (token === inspectorRequestToken.value) inspectorLoading.value = false;
  }
};
```

### 5.3 反模式

- ❌ 串行 `await`（耗时累加）
- ❌ 无竞态保护（快速切换行时旧请求覆盖新数据，用 `inspectorRequestToken` 计数器）
- ❌ 没有 loading 态（用户重复点击）
- ❌ 没有错误处理 / 私自引入前端 mock 降级

---

## 6. 复杂校验（Form Validation）

### 6.1 同步校验

```javascript
const formRules = {
  code: [
    { required: true, message: t('common.required'), trigger: 'blur' },
    { pattern: /^[A-Z0-9-]+$/, message: t('module.entity.codePattern'), trigger: 'blur' },
  ],
  name: [
    { required: true, message: t('common.required'), trigger: 'blur' },
    { max: 100, message: t('common.maxLength', { n: 100 }), trigger: 'blur' },
  ],
};
```

### 6.2 异步校验（编码唯一性）

```javascript
const validateCodeUnique = async (rule, value, callback) => {
  if (!value) return callback();
  if (mode.value === 'edit' && value === originalCode.value) return callback();
  try {
    const res = await checkCodeUnique(value);
    if (res.data.data.exists) {
      callback(new Error(t('module.entity.codeExists')));
    } else {
      callback();
    }
  } catch {
    callback();  // 网络错误不阻塞，由后端二次校验兜底
  }
};

const formRules = {
  code: [
    { required: true, message: t('common.required'), trigger: 'blur' },
    { validator: validateCodeUnique, trigger: 'blur' },
  ],
};
```

### 6.3 跨字段校验

```javascript
const validateEndDate = (rule, value, callback) => {
  if (value && formData.value.startDate && value < formData.value.startDate) {
    callback(new Error(t('module.entity.endDateBeforeStart')));
  } else {
    callback();
  }
};
```

### 6.4 提交前手动触发

```javascript
const submit = async () => {
  await formRef.value.validate();  // 全字段校验，失败抛错
  submitting.value = true;
  try {
    await (mode.value === 'create' ? create(formData.value) : update(formData.value));
    ElMessage.success(t('common.saveSuccess'));
    emit('success');
  } catch (err) {
    ElMessage.error(err.message || t('common.saveFailed'));
  } finally {
    submitting.value = false;
  }
};
```

### 6.5 反模式

- ❌ 仅前端校验无后端校验（用户可绕过）
- ❌ 不区分 trigger（全部 `blur` 导致输入时不响应；全部 `change` 导致频繁请求）
- ❌ 异步校验阻塞 submit（应在 blur 时校验，submit 时仅做最终验证）
- ❌ 错误用 `ElMessage` 而非字段级 `el-form-item__error`（位置错位）

---

## 7. 表格列渲染（Custom Column）

### 7.1 三种渲染机制（明确选一种）

| 机制 | 适用 | 示例 |
|---|---|---|
| `formatter` | 简单值转换（日期、数字格式化） | `formatter: (row) => dayjs(row.createTime).format('YYYY-MM-DD')` |
| `dicData` + 自动转换 | 字典翻译 | `dicData: createStatusDic(t)` |
| 自定义 slot | 复杂渲染（Badge、按钮、链接） | `<template #status="{ row }">` |

### 7.2 决策原则

- 字典翻译优先 `dicData`
- 单值格式化优先 `formatter`
- 含交互 / 多元素 / 状态色 用 slot
- **不混用**：同一列只用一种机制

### 7.3 反模式

- ❌ slot 内再调 `formatter`（重复逻辑）
- ❌ `dicData` + `formatter` 同时定义（行为不可预测）
- ❌ slot 内放 `<el-button @click>` 但操作逻辑分散在多处（应抽取到 `handleRowAction`）

---

## 8. 行操作（Row Actions）

### 8.1 推荐模式

```vue
<template #menu="{ row }">
  <el-button link type="primary" @click="openDetail(row)">
    {{ t('common.detail') }}
  </el-button>
  <el-button
    link
    type="primary"
    v-if="permissionValue.edit && canEdit(row)"
    @click="openEdit(row)"
  >
    {{ t('common.edit') }}
  </el-button>
  <el-popconfirm
    v-if="permissionValue.delete && canDelete(row)"
    :title="t('common.deleteConfirm')"
    @confirm="handleDelete(row)"
  >
    <template #reference>
      <el-button link type="danger">{{ t('common.delete') }}</el-button>
    </template>
  </el-popconfirm>
</template>
```

### 8.2 多操作折叠

```vue
<template #menu="{ row }">
  <el-button link type="primary" @click="openDetail(row)">{{ t('common.detail') }}</el-button>
  <el-dropdown @command="(cmd) => handleRowCommand(cmd, row)">
    <el-button link type="primary">
      {{ t('common.more') }} <el-icon><ArrowDown /></el-icon>
    </el-button>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item command="edit" v-if="permissionValue.edit">{{ t('common.edit') }}</el-dropdown-item>
        <el-dropdown-item command="copy">{{ t('common.copy') }}</el-dropdown-item>
        <el-dropdown-item command="export">{{ t('common.export') }}</el-dropdown-item>
        <el-dropdown-item command="delete" divided v-if="permissionValue.delete">
          <span class="text-danger">{{ t('common.delete') }}</span>
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>
```

### 8.3 规范

- 删除操作 + `el-popconfirm` 或 `ElMessageBox.confirm` 二次确认
- 每个按钮 `v-if="permissionValue.xxx"` + 业务条件 `canXxx(row)`
- 操作 ≥ 4 个时折叠为 dropdown（保留最常用的 1-2 个直接显示）
- dropdown 中危险操作（删除）用 `divided` 分隔且用红色

---

## 9. 搜索筛选（Quick Filters）

### 9.1 推荐模式

```vue
<div class="quick-filter-bar">
  <el-form :model="quickFilters" inline @submit.prevent="applyQuickFilters">
    <el-form-item :label="t('common.keyword')">
      <el-input
        v-model="quickFilters.keyword"
        :placeholder="t('module.entity.searchPlaceholder')"
        clearable
        @keyup.enter="applyQuickFilters"
        @clear="applyQuickFilters"
      >
        <template #prefix><el-icon><Search /></el-icon></template>
      </el-input>
    </el-form-item>
    <el-form-item :label="t('module.entity.status')">
      <el-select v-model="quickFilters.status" clearable :placeholder="t('common.all')" @change="applyQuickFilters">
        <el-option v-for="item in statusDic" :key="item.value" :label="item.label" :value="item.value" />
      </el-select>
    </el-form-item>
    <el-form-item :label="t('module.entity.dateRange')">
      <el-date-picker
        v-model="quickFilters.dateRange"
        type="daterange"
        :start-placeholder="t('common.startDate')"
        :end-placeholder="t('common.endDate')"
        value-format="YYYY-MM-DD"
        @change="applyQuickFilters"
      />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="applyQuickFilters">{{ t('common.search') }}</el-button>
      <el-button @click="resetQuickFilters">{{ t('common.reset') }}</el-button>
    </el-form-item>
  </el-form>
</div>
```

### 9.2 规范

- 3-5 个高频字段，超出移到 NativeSearchTable 列头筛选
- 与 `v-model:search` **共用同一个** search 对象
- 关键字字段支持 Enter 触发
- Select / DatePicker 支持 change 自动触发
- 重置必须清空所有字段并重新加载

### 9.3 反模式

- ❌ 维护两套 query 状态（quickFilters + search）
- ❌ 关键字输入每次按键都触发请求（应 Enter 或 debounce）
- ❌ 重置不清空 NativeSearchTable 内部 search 状态

---

## 10. 上传组件（Upload）

### 10.1 推荐模式（图片）

```vue
<el-form-item :label="t('module.entity.logo')" prop="logo">
  <el-upload
    v-model:file-list="fileList"
    :action="uploadAction"
    :headers="uploadHeaders"
    :data="uploadData"
    list-type="picture-card"
    :limit="1"
    :on-success="handleUploadSuccess"
    :on-error="handleUploadError"
    :before-upload="beforeUpload"
    :on-exceed="handleExceed"
    accept="image/jpeg,image/png,image/webp"
  >
    <el-icon><Plus /></el-icon>
    <template #tip>
      <div class="el-upload__tip">{{ t('common.upload.imgTip', { size: '5MB', formats: 'JPG/PNG/WEBP' }) }}</div>
    </template>
  </el-upload>
</el-form-item>

<script setup>
const uploadAction = `${import.meta.env.VITE_BASE_API}/blade-resource/oss/endpoint/put-file`;
const uploadHeaders = computed(() => ({ 'Blade-Auth': `bearer ${getToken()}` }));

const beforeUpload = (file) => {
  if (file.size > 5 * 1024 * 1024) {
    ElMessage.error(t('common.upload.tooLarge'));
    return false;
  }
  return true;
};

const handleUploadSuccess = (res) => {
  if (res.success) {
    formData.value.logo = res.data.link;
  } else {
    ElMessage.error(res.msg);
  }
};
</script>
```

### 10.2 规范

- 用平台 `blade-resource/oss` 接口，不引入新 OSS SDK
- 必须 `before-upload` 校验大小、类型
- 必须处理错误回调
- `v-model:file-list` 控制显示，`formData` 存最终 URL

---

## 11. 复杂日期场景（Date / DateTime / DateRange）

### 11.1 决策

| 场景 | 推荐组件 | 配置 |
|---|---|---|
| 单日 | `el-date-picker type="date"` | `value-format="YYYY-MM-DD"` |
| 月份 | `el-date-picker type="month"` | `value-format="YYYY-MM"` |
| 年份 | `el-date-picker type="year"` | `value-format="YYYY"` |
| 日期+时间 | `el-date-picker type="datetime"` | `value-format="YYYY-MM-DD HH:mm:ss"` |
| 日期范围 | `el-date-picker type="daterange"` | `value-format="YYYY-MM-DD"` |
| 时间点 | `el-time-picker` | `value-format="HH:mm:ss"` |

### 11.2 与后端约定

- 所有日期字段 `value-format` 必须与后端 `@JsonFormat` 一致（参见 industrial-page-standard 自检 §3.4）
- 范围字段后端接受两个独立参数（`startDate` / `endDate`）或单字段数组，两端统一

---

## 12. 主从联动（Master-Detail）

### 12.1 Split View 模式

```vue
<div class="im-split-view">
  <div class="im-split-view__left" :style="{ width: `${leftWidth}%` }">
    <NativeSearchTable
      v-model:search="search"
      v-model:page="page"
      :data="data"
      :option="masterOption"
      @row-click="handleRowClick"
    />
  </div>
  <div class="im-split-view__resizer" @mousedown="startResize"></div>
  <div class="im-split-view__right" :style="{ width: `${100 - leftWidth}%` }">
    <DetailPanel v-if="selectedRow" :id="selectedRow.id" />
    <el-empty v-else :description="t('common.selectToView')" />
  </div>
</div>
```

### 12.2 规范

- 默认比例 70:30
- 分割条可拖拽（最小 30%，最大 80%）
- 选中行高亮（用 `:row-class-name="getRowClass"`）
- 右侧空状态明确引导

### 12.3 Sub-table Tabs 模式

表单页或详情页内嵌子表列表，使用 Tabs 切换多张子表：

```vue
<div class="im-section-card--tabs">
  <div class="im-section-head--tabs">
    <el-tabs v-model="activeTab" type="card">
      <el-tab-pane :label="t('module.entity.childTable1')" name="child1" />
      <el-tab-pane :label="t('module.entity.childTable2')" name="child2" />
    </el-tabs>
    <div class="tab-actions">
      <el-button size="small" @click="handleTabAction('add')">
        <el-icon><Plus /></el-icon> {{ t('common.add') }}
      </el-button>
    </div>
  </div>
  <div class="im-section-body--tabs">
    <Child1List v-if="activeTab === 'child1'" ref="child1ListRef" :parent-id="formData.id" />
    <Child2List v-if="activeTab === 'child2'" ref="child2ListRef" :parent-id="formData.id" />
  </div>
</div>
```

**规范**:
- 子表组件通过 `ref` 暴露 `reload()` 方法，父表保存成功后调用
- Tab header 右侧放置子表级操作按钮（新增/导入/导出）
- 参考实现: `src/views/partner/enterprise/form.vue`

### 12.4 Section Navigator（Dot Navigator）

长表单页侧边点导航器，基于 IntersectionObserver 自动高亮当前可见分段：

```javascript
// setSectionRef: 函数式 ref 收集
const sectionRefs = {};
const setSectionRef = (key) => (el) => { if (el) sectionRefs[key] = el; };

// IntersectionObserver 初始化
const initSectionObserver = () => {
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((e) => {
        if (e.isIntersecting) activeSectionIdx.value = sectionList.value.findIndex(s => s.key === e.target.dataset.sectionKey);
      });
    },
    { root: formBodyRef.value, threshold: 0.3 }
  );
  Object.entries(sectionRefs).forEach(([key, el]) => {
    el.dataset.sectionKey = key;
    observer.observe(el);
  });
};

// 点击导航跳转
const scrollToSection = (key) => sectionRefs[key]?.scrollIntoView({ behavior: 'smooth', block: 'start' });
```

**规范**:
- 当 section 数量 ≥ 3 时默认启用
- 导航器固定在表单体左侧，使用 `position: sticky`
- 高亮色使用 `var(--eam-accent, var(--im-color-primary))`
- 参考实现: `src/views/partner/enterprise/form.vue`

### 12.5 Status Badge Mapper

统一的状态/类型字段 badge 渲染模式：

```javascript
// option/<module>/<entity>.js
const STATUS_MAP = {
  DRAFT:     { label: 'status.draft',     type: 'info' },
  ACTIVE:    { label: 'status.active',    type: 'success' },
  SUSPENDED: { label: 'status.suspended', type: 'warning' },
  CLOSED:    { label: 'status.closed',    type: 'danger' },
};

export const getStatusBadgeType = (val) => STATUS_MAP[val]?.type || 'info';
export const getStatusLabel = (val, t) => t(STATUS_MAP[val]?.label || 'status.unknown');

// 列表列 formatter 用法
export const createTableOption = (t) => ({
  columns: [
    {
      prop: 'status', label: t('fields.status'),
      formatter: (row) => getStatusLabel(row.status, t),
      badgeClass: (row) => `im-badge--${getStatusBadgeType(row.status)}`,
    },
  ],
});
```

**规范**:
- 所有状态字段必须通过 mapper 渲染，禁止在模板中硬编码条件
- Mapper 定义在 option 文件中，与列配置同文件
- Badge 类名统一使用 `im-badge--{type}` (success/warning/danger/info/primary)

---

## 13. 性能优化模式

| 场景 | 推荐 |
|---|---|
| > 200 行表格 | `el-table-v2` 或服务端分页 |
| > 200 项下拉 | `el-select-v2` |
| 频繁更新的数据流 | `shallowRef` + 手动 trigger |
| 大表单字段 | section 折叠 + 按需渲染 |
| 复杂图表 | `defineAsyncComponent` 异步加载 |
| 详情页多接口 | `Promise.all` + `inspectorRequestToken` 竞态保护 |
| 列表筛选输入 | debounce 300ms |
| 滚动监听 | `IntersectionObserver` 或 `animation-timeline` |

---

## 14. 国际化模式

### 14.1 静态文案

```vue
<el-button>{{ t('common.add') }}</el-button>
```

### 14.2 动态插值

```javascript
ElMessage.success(t('module.entity.deleteSuccess', { name: row.name }));
```

### 14.3 字典工厂

```javascript
// option/<module>/<entity>.js
export const createStatusDic = (t) => [
  { label: t('module.entity.status.active'), value: 'ACTIVE', tagType: 'success' },
  { label: t('module.entity.status.inactive'), value: 'INACTIVE', tagType: 'info' },
];
```

### 14.4 双语同步

新增 `zh.js` key 必须在同次提交追加 `en.js`。

---

## 15. 模式扩展流程

新增高频组合时：

1. 在本文件追加新章节，包含 ✅ 推荐模式 + ❌ 反模式 + 决策原则
2. 在 `anti-patterns.md` §5 同步追加对应反模式
3. 通知 `Copilot Code Review` 加入审查清单
4. 提供至少一个生产页面作为参考实现
