---
globs: ["**/*.vue", "src/components/**", "src/views/**"]
---

# Vue 组件规范

## 组件命名
- 组件名必须为多个单词，禁止单个单词（如 `Todo`），使用 `TodoItem`
- 组件文件名使用 PascalCase（如 `MyComponent.vue`）
- 基础组件以 `Yl` 前缀命名：`YlButton.vue`、`YlTable.vue`、`YlIcon.vue`
- 单例组件以 `The` 前缀命名：`TheHeading.vue`、`TheSidebar.vue`
- 紧密耦合的子组件以父组件名为前缀：`SearchSidebar.vue` → `NavigationForSearchSidebar.vue`
- 组件名以高级别单词开头，描述性修饰词结尾：`SearchButtonClear.vue`、`SettingsCheckboxTerms.vue`
- 使用完整单词，禁止缩写：❌ `UProfOpts.vue` → ✅ `UserProfileOptions.vue`
- JS 中组件名使用 PascalCase：`name: 'MyComponent'`

## 组件结构
- 单文件组件顶级元素顺序：`<template>` → `<script>` → `<style>`，`<style>` 始终放最后
- 每个组件单独一个文件，禁止在一个文件中注册多个组件
- 组件选项顺序：`props` → `data` → `computed` → `watch` → `created` → `mounted` → `methods`

## data 定义
- 组件的 `data` 必须是函数返回对象，禁止直接定义为对象（根实例除外）
```javascript
// ✅ 正确
data() { return { foo: 'bar' } }
// ❌ 错误
data: { foo: 'bar' }
```

## Prop 规范
- Prop 定义必须尽量详细，包含 `type`、`required`、必要时包含 `validator`
- Prop 名在 JS 中使用 camelCase，在模板中使用 kebab-case
```javascript
props: { greetingText: { type: String, required: true } }
```
```html
<WelcomeMessage greeting-text="hi"/>
```

## 模板规范
- `v-for` 必须设置 `:key`
- 禁止 `v-if` 和 `v-for` 用在同一元素上；通过计算属性过滤或外层容器 `v-if`
- 多个 attribute 的元素分多行撰写，每个 attribute 一行
- 模板中只包含简单表达式，复杂逻辑移入 computed 或 methods
- HTML attribute 值始终带引号
- 自闭合组件使用简洁写法：`<MyComponent/>`
- 模板中组件名使用 PascalCase（SFC 和字符串模板中）或 kebab-case（DOM 模板中）
- `v-if/v-else-if/v-else` 中使用 `key` 区分元素

## 指令与绑定
- 指令缩写保持统一：全部使用 `:` / `@` / `#`，或全部不缩写
- 属性顺序：`is` → `v-for` → `v-if/v-else/v-show` → `v-pre/v-once` → `id` → `ref/key` → `v-model` → 其他属性 → `v-on` → `v-html/v-text`

## 样式规范
- 组件样式必须使用 `scoped` 设置作用域
- scoped 中禁止使用元素选择器（如 `button {}`），使用类选择器
- 复杂计算属性拆分为多个简单 property

## 组件通信
- 优先通过 prop 和事件（`$emit`）进行父子组件通信
- 禁止使用 `this.$parent` 或直接变更 prop
- 禁止在子组件中直接修改父组件数据
```javascript
// ✅ 正确：通过事件通信
template: `<input :value="todo.text" @input="$emit('input', $event.target.value)">`
// ❌ 错误：直接修改 prop
template: '<input v-model="todo.text">'
```

## 私有属性
- 使用模块作用域保持私有函数的私有性
- 如需在 mixin 中使用私有方法，使用 `$_pluginName_` 命名空间前缀

## 项目封装组件（必须使用，禁止直接使用 Element UI 原生组件）

| 禁止使用 | 应替换为 | 注册方式 |
|---------|---------|---------|
| `el-dialog` | `yl-dialog` | 需 import 并注册 |
| `el-upload` | `DefaultUpload` | 需 import 并注册 |
| `el-form` + `el-table`（搜索列表页） | `yl-search-form` + `yl-table` | 全局注册，无需 import |
| 自定义 `Page` 分页组件 | yl-table 内置分页 | yl-table 自带分页 |

### yl-dialog
```vue
<yl-dialog :visible.sync="dialogVisible"
    :attributes="{width: '520px', title: '标题'}"
    :closeOnClickModal="false"
    @close="handleClose">
    <!-- 内容 -->
    <div slot="footer"><!-- 底部按钮 --></div>
</yl-dialog>
```
```javascript
import ylDialog from '@/main/components/ylDialog.vue';
export default { components: { ylDialog } }
```

### DefaultUpload
```vue
<DefaultUpload ref="upload" text="选择文件"
    textTip="仅支持 .xlsx/.xls 格式"
    authType="excel" :limit="1"
    :autoUpload="false" :showFileList="true"
    @handleChange="handleFileChange">
</DefaultUpload>
```
```javascript
import DefaultUpload from '@/main/components/defaultUpload.vue';
export default { components: { DefaultUpload } }
```

### yl-search-form + yl-table
```vue
<yl-search-form :searchParmas="searchFormConfig" ref="ylSearchForm"
    :tableRefs="$refs" tableRefsName="ylTable" :isCard="true"
    @getFullData="handleGetFullData">
</yl-search-form>

<yl-table :data="ylTableData" ref="ylTable"
    :search="$refs" searchRefsName="ylSearchForm"
    @selectRowData="handleSelectionChange">
    <template #slotName="{ data }">{{ data.row.field }}</template>
</yl-table>
```
- 全局注册，无需 import
- `searchFormConfig.request`: `{ url, type: 'axiosJson', method: 'post', options: {...} }`
- 日期范围用管道分隔键名: `'startDate|endDate': ''`
- `ylTableData.header`: 扁平数组，**禁止嵌套分组列头**
- 分组信息写入列标签前缀（如"企业端费用方案编号"、"财司通道端大区"）
- 自动管理请求/分页/重置，无需手写 `fetchData` / `handlePageChange` 等方法
- yl-table 日期列声明式配置（无需写 slot）：
```javascript
{ prop: 'operateTime', label: '操作时间', width: 160,
  attr: { type: 'date', format: 'yyyy-MM-dd HH:mm:ss' } }
```

## 枚举显示规范（禁止硬编码枚举映射）

模板中显示枚举值时，禁止使用硬编码三元表达式或本地 MAP 常量，必须通过 Vuex store getter 统一获取。

> 下例中 `{feModule}`（Vuex 模块名）、`get{FeModule}Status`（枚举 getter 名）、枚举 key 均为**示例占位**，落地时替换为本服务前端实际的 Vuex 模块名、getter 名与枚举 key（前端命中时按其前端代码/接口文件确定）。

```vue
<!-- ❌ 禁止：硬编码映射 -->
{{ data.row.status === '1' ? '已处理' : data.row.status === '2' ? '已驳回' : '--' }}

<!-- ❌ 禁止：本地定义 MAP 常量 -->
var STATUS_MAP = { '0': '待处理', '1': '已处理' };

<!-- ✅ 正确：通过 Vuex getter -->
{{ $store.getters.get{FeModule}Status(data.row.status, 'orderStatus') || '--' }}
{{ $store.getters.get{FeModule}Status(data.row.yesNoFlag, 'yesNo') || '--' }}
```
```javascript
// 搜索表单选项引用 Vuex state（禁止硬编码选项数组）
data: this.$store.state.{feModule}.orderStatus
```

本服务前端 Vuex 模块（`{feModule}`）中按业务定义所需枚举 key（示例：`orderStatus`、`bizType`、`yesNo` 等），在模块内集中维护。

getter `get{FeModule}Status(code, stateKey)` 建议使用 `==` 松比较，支持 string/number 混合匹配。

## 常见错误模式
- ❌ 单词组件名 `Todo` → ✅ `TodoItem`
- ❌ `data: { foo: 'bar' }` → ✅ `data() { return { foo: 'bar' } }`
- ❌ `v-for` 不带 `:key` → ✅ `v-for="item in items" :key="item.id"`
- ❌ 同一元素 `v-for` + `v-if` → ✅ 使用 computed 过滤或外层 `v-if`
- ❌ `<style>` 无 scoped → ✅ `<style scoped>`
- ❌ 使用 `el-dialog` → ✅ 使用 `yl-dialog`，通过 `:attributes` 传递 title/width
- ❌ 使用 `el-upload` → ✅ 使用 `DefaultUpload`
- ❌ 手写 `el-form` + `el-table` 搜索列表 → ✅ 使用 `yl-search-form` + `yl-table`
- ❌ 硬编码枚举 `status === '1' ? '已处理' : ...` → ✅ `$store.getters.get{FeModule}Status(val, 'orderStatus')`
- ❌ 本地定义 `PERF_STATUS_MAP` 等枚举常量 → ✅ 引用 Vuex state/getter
