---
description: 组件开发规范 - 文件组织、Props 设计、布局策略、BEM 命名。适用于所有 components 目录。
globs: ["**/components/**/*.vue", "**/components/**/*.ts"]
alwaysApply: false
---

# 组件开发规范

该规则用于组件开发：

**当创建新组件时：**

- 先查公共组件/类清单（@foundation/project.mdc）
- 使用 BEM 命名 + scoped 样式
- 组件目录与组件名一致（PascalCase）

**当处理布局时：**

- 优先使用 `flex + gap` 替代 `margin`
- gap 不一致时分组嵌套

**当定义 Props 时：**

- ≤5 个散开，>5 个收敛为 item
- 列表数据必须 Props 化 + `v-for`

---

## 📁 文件组织

**方案 A：单文件组件**（业务项目推荐）

```
{组件目录}/UserCard/
├── index.vue      # 组件实现
└── types.ts       # 类型定义（可选）
```

**方案 B：统一导出**（组件库推荐）

```
{组件目录}/UserCard/
├── index.ts       # 统一导出入口
├── UserCard.vue   # 组件实现
└── types.ts       # 类型定义
```

> 📋 具体存放路径详见 @foundation/project.mdc

---

## 🎨 设计策略

### 1. 优先复用

**复用优先级**：

1. 先查项目公共组件（如已有 `Avatar`，直接用）
2. 再查项目公共类（如已有 `.ellipsis`，直接用）
3. 都没有再自己写（BEM 类名 + scoped 样式）

> 📋 公共组件/类清单详见 @foundation/project.mdc

### 2. Flex + Gap 布局

**原则**：优先使用 `flex + gap` 替代 `margin`，尽可能使用 `class` 而不是 `style`

**gap vs margin 选择**：

| 场景             | 推荐               | 原因                                 |
| ---------------- | ------------------ | ------------------------------------ |
| 同级元素等间距   | `gap`              | 更简洁，自动处理首尾                 |
| 需要 RTL 支持    | `gap`              | 自动适配，无需切换 margin-left/right |
| 单个元素独立偏移 | `margin`           | 如 `margin-left: auto` 推右侧        |
| 与外部容器间距   | `margin`/`padding` | gap 只作用于子元素之间               |

**固定 gap vs 动态分布**：

| 场景                    | 推荐                       | 示例                               |
| ----------------------- | -------------------------- | ---------------------------------- |
| 设计稿标注固定间距      | `gap`                      | 头像和名字间隔 0.32rem             |
| 两端对齐，中间均分      | `space-between`            | 导航栏、底部 Tab 栏、列表item左-右 |
| 固定间距 + 剩余空间推开 | `gap` + `margin-left:auto` | 左侧 logo + 右侧按钮               |

**分组决策**：

| 步骤 | 决策点   | 判断依据                                   |
| ---- | -------- | ------------------------------------------ |
| 1️⃣   | 视觉分组 | 设计稿中形成一个视觉单元？                 |
| 2️⃣   | 功能分组 | 可能需要整体 `v-if` / `@click` / `hover`？ |
| 3️⃣   | 间距优化 | 组内：一致 → 平铺，不一致 → 继续嵌套       |

**示例**：

```
输入：【头像 0.32rem 名字 0.32rem 金币 0.32rem 排名】

分析：
1. 视觉：头像+名字 是一个单元，金币+排名 是另一个
2. 功能：用户区可能需要整体点击跳转
3. 间距：组内均 0.32rem → 平铺

输出：【【头像 名字】【金币 排名】】
```

```vue
<template>
  <div class="u-card__row">
    <div class="u-card__user">
      <Avatar :src="user.avatar" />
      <span>{{ user.name }}</span>
    </div>
    <div class="u-card__stats">
      <span>{{ user.coins }}</span>
      <span>{{ user.rank }}</span>
    </div>
  </div>
</template>

<style scoped>
.u-card__row {
  display: flex;
  gap: 0.64rem;
}
.u-card__user {
  display: flex;
  gap: 0.32rem;
}
.u-card__stats {
  display: flex;
  gap: 0.32rem;
}
</style>
```

### 3. BEM 类名收敛

**根类名**：简写 + 有特征

```scss
// ✅ 简写 + 有特征
.u-card {
} // user-card
.pwr-card {
} // power-card

// ❌ 避免：通用词（易重复）
.card {
}
.item {
}
.header {
}
```

**子元素**：简洁

```scss
// ✅ 简洁
.u-card__avatar {
}
.u-card__name {
}

// ❌ 避免：过长
.u-card__avatar-image-wrapper {
}
```

**状态**：`is-xxx` 或 `--modifier`

```scss
.u-card.is-active {
}
.u-card--large {
}
```

### 4. Props 策略

**散开 vs 收敛**：

| 条件       | 散开 props         | 收敛 props（item）      |
| ---------- | ------------------ | ----------------------- |
| props 数量 | ≤ 5 个             | > 5 个                  |
| 数据关联性 | 无强关联           | 是完整实体              |
| 组件类型   | 通用 UI 组件       | 业务展示组件            |
| 示例       | `Button`、`Dialog` | `UserCard`、`OrderItem` |

```vue
<!-- 散开：通用组件 -->
<AppButton type="primary" size="small" :disabled="loading" />

<!-- 收敛：业务组件 -->
<UserCard :user="userInfo" />
```

### 5. 列表数据 Props 化

| 特征                    | 判断     | 实现方式               |
| ----------------------- | -------- | ---------------------- |
| ≥2 个相似结构（如 FAQ） | 列表数据 | `v-for` + `Props<T[]>` |
| 单个静态内容（如 Logo） | 固定数据 | 硬编码或常量           |

```vue
<!-- ✅ 正确：Props 化 -->
<div v-for="(item, index) in faqList" :key="index" class="faq__item">
  <div class="faq__title">{{ index + 1 }}. {{ item.title }}</div>
</div>

<!-- ❌ 错误：硬编码重复结构 -->
<div class="faq__item"><div class="faq__title">1. What is xxx?</div></div>
<div class="faq__item"><div class="faq__title">2. How to xxx?</div></div>
```

---

## ✅ 检查清单

### 文件组织

- ✅ 组件目录与组件名一致（PascalCase）
- ✅ 统一采用方案 A 或方案 B，项目内不可混用

### Props & Emits

- ✅ Props 和 Emits 必须明确类型定义
- ❌ 禁止组件直接调用 API（通过 props 传入数据）
- ❌ 禁止组件直接访问 Store（通过 props/emits 通信）

### 样式

- ✅ 组件样式必须使用 `scoped` 或 CSS Modules
- ✅ BEM 根类名简写 + 有特征，避免通用词
- ❌ 禁止组件样式污染全局

### 逻辑

- ❌ 禁止组件包含业务逻辑（应放在页面或 hooks）
- ❌ 禁止将列表数据硬编码为重复 DOM（使用 `v-for`）

---

## 📋 标准模板

```vue
<template>
  <div class="u-card">
    <img class="u-card__avatar" :src="user.avatar" />
    <div class="u-card__info">
      <div class="u-card__name">{{ user.name }}</div>
      <div class="u-card__desc">{{ user.desc }}</div>
    </div>
  </div>
</template>

<script setup lang="ts">
interface Props {
  user: UserInfo;
}
interface Emits {
  (e: "click", id: number): void;
}

const props = defineProps<Props>();
const emit = defineEmits<Emits>();
</script>

<style scoped>
.u-card {
  display: flex;
  gap: 0.32rem;
}
.u-card__avatar {
  width: 1rem;
  height: 1rem;
}
</style>
```
