---
name: page-codegen
description: 'Use when: generating Vue 3 page code from either a page-spec JSON or a natural language description. Outputs index.vue + data.ts + index.scss following Robot Admin conventions. Triggers on: page generation, code generation, 生成页面, 代码生成, vue页面, codegen, 页面骨架, scaffold, 建个页面, 写个页面, 帮我做个页面, 口述需求, natural language page request.'
---

# Skill: 页面代码生成（page-codegen）

根据 **page-spec JSON**（来自 prototype-scan / api-contract）自动生成完整的 Vue 3 页面代码，
严格遵循 Robot Admin 项目编码规范。

---

## 触发

| 模式                    | 输入                                  | 何时使用             |
| ----------------------- | ------------------------------------- | -------------------- |
| **模式 0（自然语言）**  | 用户口述页面需求，无 JSON 文件        | 日常对话直接提需求时 |
| **模式 1（JSON 输入）** | 来自 prototype-scan 的 page-spec JSON | 有原型/详设文档时    |

- 输出统一：`index.vue` + `data.ts` + `index.scss`（三件套）
- 可选追加：`layouts/` 目录（多布局变体）、composables（复杂业务逻辑）

---

## 模式 0 — 自然语言转 page-spec（内部步骤）

用户口述需求时，AI **先在内部完成以下推导**，不必向用户索要 JSON，直接进入生成流程。

### 推导步骤

**① 提取页面基础信息**

从用户描述中识别：

- 页面中文名 / 所属模块（domain）
- 核心资源名称（resource），驼峰命名
- 交互模式（参照下方模式映射表）

**② 关键词 → 交互模式映射**

| 用户描述中出现的关键词              | 映射模式                  |
| ----------------------------------- | ------------------------- |
| 列表、查询、搜索 + 表格             | `LIST`                    |
| 左侧树 + 右侧表格                   | `TREE_LIST`               |
| 主表 + 明细 / 上下两个表格          | `MASTER_DETAIL`           |
| 独立表单页 / 多 Tab 表单 / 步骤表单 | `FORM_PAGE`               |
| 弹窗表单 / 新增编辑弹窗             | `LIST`（含 `FORM_MODAL`） |
| 详情页 + 子表 Tab                   | `DETAIL_TABS`             |
| 统计 / 大屏 / 图表                  | `DASHBOARD`               |

**③ 字段推导**

- 用户提到的字段名 → camelCase 字段名 + 推断组件类型（input/select/date...）
- 用户提到"状态"类字段 → 自动生成 `STATUS_TAG_CONFIG`，固定右侧列
- 未提及但常规必有的字段 → 自动补充 `id`、`createTime`、`status`

**④ 内部生成 page-spec 骨架（仅用于后续步骤，不输出给用户）**

```json
{
  "pageName": "推断的页面名",
  "domain": "推断的模块路径",
  "resource": "推断的资源名",
  "mode": "推断的交互模式",
  "columns": [],
  "form": [],
  "query": [],
  "toolbar": [],
  "operations": []
}
```

**⑤ 不确定时的处理原则**

- 模式不明确 → 优先推断为 `LIST`（最常见）
- 字段描述模糊 → 用 `input` 类型占位，代码注释标注 `// TODO: 请确认字段类型`
- 接口路径未提及 → 按 `/<domain>/<resource>` 惯例自动生成，注释提醒替换
- **不向用户索要 JSON，直接用推断结果生成代码**

---

## 输出文件结构

根据页面模式（mode），输出不同复杂度的文件结构：

### 简单页面（LIST / FORM_MODAL）

```
src/views/<domain>/<module-name>/
├── index.vue      # 页面主文件
├── data.ts        # 类型 + 配置 + 常量
└── index.scss     # 页面样式
```

### 中等复杂页面（TREE_LIST / MASTER_DETAIL）

```
src/views/<domain>/<module-name>/
├── index.vue
├── data.ts
├── index.scss
└── components/          # 局部子组件
    ├── DetailDrawer.vue
    └── FormModal.vue
```

### 高度复杂页面（FORM_PAGE / DETAIL_TABS / COMPOSITE）

```
src/views/<domain>/<module-name>/
├── index.vue
├── data.ts
├── index.scss
├── components/
│   ├── BasicInfo.vue
│   └── SubTable.vue
└── composables/         # 业务逻辑
    └── useXxxLogic.ts
```

---

## data.ts 生成规则

### 区块顺序（固定）

```typescript
/*
 * @Author: ChenYu ycyplus@gmail.com
 * @Date: {{date}}
 * @Description: {{页面中文名}} — 数据配置
 * Copyright (c) {{year}} by CHENY, All Rights Reserved 😎.
 */

import type { SelectOption, DataRecord } from '@robot-admin/naive-ui-components'
import type { TableColumn, UseTableCrudConfig } from '@robot-admin/request-core/naive'
import { PRESET_RULES } from '@robot-admin/form-validate'

// ================= 业务类型定义 =================
// 从 page-spec.columns + page-spec.form 合并字段生成 interface

export interface {{ResourceName}} extends DataRecord {
  id: number | string
  // ... 所有字段
}

export interface {{ResourceName}}FormData {
  // ... 表单字段（id 可选）
}

export interface SearchForm {
  // ... 查询字段
}

// ================= 选项配置 =================
// 下拉框 / 筛选器的选项数据

export const STATUS_OPTIONS: SelectOption[] = [
  { label: '启用', value: 1 },
  { label: '禁用', value: 0 },
]

// ================= Tag 映射配置 =================
// 用于状态类列渲染彩色标签

export const STATUS_TAG_CONFIG: Record<string | number, {
  text: string
  type: 'default' | 'success' | 'error' | 'warning' | 'info'
}> = {
  1: { text: '启用', type: 'success' },
  0: { text: '禁用', type: 'error' },
}

// ================= 图标配置 =================

export const ICON_CONFIG = {
  search: 'mdi:magnify',
  plus: 'mdi:plus',
  refresh: 'mdi:refresh',
  edit: 'mdi:pencil',
  delete: 'mdi:delete',
  eye: 'mdi:eye',
  // ... 根据 page-spec.toolbar + operations 的 icon 字段
} as const

// ================= 格式化函数 =================
// 简短的格式化函数（列渲染用）

// ================= 表格列配置 =================

export const getTableColumns = (): TableColumn[] => [
  // 从 page-spec.columns 映射
  {
    key: 'fieldName',
    title: '列标题',
    width: 120,
    // 可编辑列追加 editable / editType / editProps
    // 状态列追加 render 函数（渲染 NTag）
  },
]

// ================= 表单验证规则 =================

export const FORM_RULES: FormRules = {
  // 从 page-spec.form 的 rules 字段映射
  fieldName: [
    PRESET_RULES.required('字段名'),
    // ...
  ],
}

// ================= 表单默认值 =================

export const FORM_DEFAULTS: {{ResourceName}}FormData = {
  // 所有字段的初始值
}

// ================= useTableCrud 配置 =================
// 仅 LIST 模式页面使用

export const getTableCrudConfig = (): UseTableCrudConfig => ({
  api: {
    list: '/<domain>/<resource>',
    create: '/<domain>/<resource>',
    update: '/<domain>/<resource>/:id',
    remove: '/<domain>/<resource>/:id',
    get: '/<domain>/<resource>/:id',
  },
  columns: getTableColumns(),
  defaultPageSize: 20,
})
```

### 关键映射规则

**page-spec.columns → TableColumn：**

| page-spec 字段             | TableColumn 字段                |
| -------------------------- | ------------------------------- |
| `key`                      | `key`                           |
| `title`                    | `title`                         |
| `width`                    | `width`                         |
| `sortable`                 | `sortable: true`                |
| `render: 'tag'` + `tagMap` | `render: (row) => h(NTag, ...)` |
| `render: 'text'`           | 默认文本渲染                    |

**page-spec.form → FormRules：**

| page-spec 字段          | FormRules 映射                         |
| ----------------------- | -------------------------------------- |
| `required: true`        | `PRESET_RULES.required('字段名')`      |
| `rules: 'email'`        | `PRESET_RULES.email('邮箱')`           |
| `rules: 'mobile'`       | `PRESET_RULES.mobile('手机号')`        |
| `rules: 'length(2,20)'` | `PRESET_RULES.length('字段名', 2, 20)` |
| `rules: 'range(0,100)'` | `PRESET_RULES.range('字段名', 0, 100)` |

---

## index.vue 生成规则

### 文件头

```vue
<!--
 * @Author: ChenYu ycyplus@gmail.com
 * @Date: {{date}}
 * @Description: {{页面中文名}}
 * Copyright (c) {{year}} by CHENY, All Rights Reserved 😎.
-->
```

### Script 区块顺序（固定）

```vue
<script setup lang="ts">
  // ① defineOptions
  defineOptions({ name: '{{ComponentName}}' })

  // ② 导入配置数据
  import { useNaiveTableCrud } from '@robot-admin/request-core/naive'
  import { ... } from './data'
  import { ... } from '@/api/{{domain}}-{{resource}}'

  // ③ 响应式状态
  const message = useMessage()
  const dialog = useDialog()
  const loading = ref(false)
  const searchForm = ref<SearchForm>({ ... })

  // ④ Headless CRUD + Naive 消息适配（LIST 模式）
  const tableCrud = useNaiveTableCrud(getTableCrudConfig())

  // ⑤ 计算属性

  // ⑥ 方法：查询 / CRUD / 事件处理

  // ⑦ 生命周期
  onMounted(() => { ... })
</script>
```

### 自动导入清单（无需 import）

以下 API 自动导入，生成代码中**不要写 import 语句**：

```
Vue:     ref, computed, watch, onMounted, nextTick, reactive, readonly, h
Router:  useRoute, useRouter
Pinia:   defineStore, storeToRefs
VueUse:  useLocalStorage, useClipboard, useDebounceFn
NaiveUI: NCard, NButton, NSpace, NInput, NSelect, NTag, NModal, NDrawer,
         NGrid, NGi, NTabs, NTabPane, NAlert, NSwitch, NRadioGroup, ...
         useMessage, useDialog, useNotification
Store:   s_userStore, s_themeStore, ...
```

### Template 区块：按页面模式展开

---

#### 模式 A：LIST（查询 + 表格）

```vue
<template>
  <div class="{{kebab-name}}">
    <!-- 搜索和操作栏 -->
    <NCard class="header-card">
      <NSpace
        justify="space-between"
        align="center"
      >
        <NSpace>
          <!-- 查询字段：从 page-spec.query 生成 -->
          <NInput
            v-model:value="searchForm.keyword"
            placeholder="搜索..."
            clearable
            style="width: 300px"
            @keyup.enter="handleSearch"
          >
            <template #prefix>
              <C_Icon
                :name="ICON_CONFIG.search"
                :size="16"
              />
            </template>
          </NInput>
          <!-- 更多查询字段... -->
        </NSpace>
        <!-- 工具栏按钮：从 page-spec.toolbar 生成 -->
        <C_ActionBar
          :actions="toolbarActions"
          :config="{ compact: true }"
        />
      </NSpace>
    </NCard>

    <!-- 表格 -->
    <NCard>
      <C_Table
        ref="tableRef"
        :columns="tableColumns"
        :data="tableData"
        :loading="loading"
        :row-key="(row: any) => row.id"
        :config="{
          actions: tableActions,
          pagination: {
            page: pagination.page,
            pageSize: pagination.pageSize,
            total: pagination.total,
            remote: true,
          },
          display: { scrollX: {{scrollX}} },
        }"
        @pagination-change="handlePaginationChange"
      />
    </NCard>

    <!-- 新增/编辑弹窗（FORM_MODAL） -->
    <NModal
      v-model:show="showFormModal"
      preset="card"
      :title="formMode === 'add' ? '新增' : '编辑'"
      style="width: 600px"
    >
      <C_Form
        ref="formRef"
        :options="formOptions"
        :model="formData"
        :rules="formRules"
      />
      <template #footer>
        <NSpace justify="end">
          <NButton @click="showFormModal = false">取消</NButton>
          <NButton
            type="primary"
            :loading="submitLoading"
            @click="handleSubmit"
          >
            确认
          </NButton>
        </NSpace>
      </template>
    </NModal>
  </div>
</template>
```

---

#### 模式 B：TREE_LIST（树形 + 表格）

```vue
<template>
  <div class="{{kebab-name}}">
    <NCard class="header-card">
      <!-- 搜索区 -->
    </NCard>

    <div class="main-content">
      <NGrid
        :cols="24"
        :x-gap="16"
        responsive="screen"
      >
        <!-- 左侧树 -->
        <NGi :span="6">
          <NCard
            title="分类树"
            class="content-card"
          >
            <C_Tree
              ref="treeRef"
              mode="custom"
              :data="treeData"
              :searchable="true"
              @node-select="handleTreeSelect"
            />
          </NCard>
        </NGi>
        <!-- 右侧表格 -->
        <NGi :span="18">
          <NCard class="content-card">
            <C_Table ... />
          </NCard>
        </NGi>
      </NGrid>
    </div>
  </div>
</template>
```

---

#### 模式 C：FORM_PAGE（独立表单页）

```vue
<template>
  <div class="{{kebab-name}}">
    <NCard :title="pageTitle">
      <C_Form
        ref="formRef"
        :options="formOptions"
        :model="formData"
        :rules="formRules"
        :config="{
          layout: 'tabs',
          // 或 layout: 'steps'
        }"
      />
      <template #footer>
        <NSpace justify="end">
          <NButton @click="handleBack">返回</NButton>
          <NButton
            type="primary"
            :loading="submitLoading"
            @click="handleSave"
          >
            保存
          </NButton>
        </NSpace>
      </template>
    </NCard>
  </div>
</template>
```

---

#### 模式 D：MASTER_DETAIL（主表 + 明细）

```vue
<template>
  <div class="{{kebab-name}}">
    <!-- 上方：主表 -->
    <NCard title="主表">
      <C_Table
        :columns="masterColumns"
        :data="masterData"
        ...
      />
    </NCard>
    <!-- 下方：明细表 -->
    <NCard :title="detailTitle">
      <NTabs
        v-model:value="activeTab"
        type="line"
        animated
      >
        <NTabPane
          v-for="sub in subTables"
          :key="sub.key"
          :name="sub.key"
          :tab="sub.title"
        >
          <C_Table
            :columns="sub.columns"
            :data="sub.data"
            ...
          />
        </NTabPane>
      </NTabs>
    </NCard>
  </div>
</template>
```

---

## index.scss 生成规则

```scss
// 页面容器
.{{kebab-name}} {
  display: flex;
  flex-direction: column;
  gap: 16px;

  .header-card {
    :deep(.n-card__content) {
      padding: 16px;
    }
  }

  .content-card {
    height: calc(100vh - 220px);
    overflow: auto;
  }

  // 根据页面模式追加：
  // TREE_LIST → .main-content { ... }
  // MASTER_DETAIL → .detail-section { ... }
}
```

---

## 操作列按钮生成

从 page-spec.operations 生成 tableActions 数组：

```typescript
const tableActions = computed(() => [
  {
    label: '编辑',
    type: 'primary' as const,
    icon: ICON_CONFIG.edit,
    onClick: (row: ResourceItem) => handleEdit(row),
    // 条件显示
    show: (row: ResourceItem) => row.status === 1,
  },
  {
    label: '删除',
    type: 'error' as const,
    icon: ICON_CONFIG.delete,
    onClick: (row: ResourceItem) => handleDelete(row),
    confirm: { title: '确认删除', content: '删除后不可恢复' },
  },
])
```

> ⚠️ **操作列按钮必须与 page-spec 严格一一对应，不可自编按钮**

---

## 状态列渲染

状态类列必须用 NTag 彩色标签渲染：

```typescript
// data.ts
export const STATUS_TAG_CONFIG = {
  active: { text: '在职', type: 'success' },
  inactive: { text: '离职', type: 'error' },
  probation: { text: '试用期', type: 'warning' },
} as const

// 列定义中
{
  key: 'status',
  title: '状态',
  width: 100,
  render: (row: Employee) => {
    const config = STATUS_TAG_CONFIG[row.status]
    return config ? h(NTag, { type: config.type, size: 'small' }, () => config.text) : '-'
  },
}
```

---

## 生成校验 Checklist

- [ ] `defineOptions({ name: '...' })` 存在
- [ ] 文件头注释完整（Author/Date/Description）
- [ ] JSDoc 注释覆盖所有导出函数
- [ ] 请求函数从 `@robot-admin/request-core/axios` 导入，Naive CRUD 从 `/naive` 导入
- [ ] 使用 `@robot-admin/form-validate` 的 PRESET_RULES
- [ ] 使用 `@robot-admin/naive-ui-components` 的 C_Table / C_Form / C_ActionBar
- [ ] 自动导入的 API 未被手动 import（ref/computed/NCard/useMessage 等）
- [ ] 样式使用 `<style lang="scss" scoped>` + `@use './index.scss'`
- [ ] 操作列按钮与 page-spec 一一对应
- [ ] 状态类列使用 NTag 渲染
- [ ] 不多加功能、不多加注释、不自编按钮
