返回笔记首页

模块4.2:Skill.md文件定制 - 解决重复性工作

主题配置

一、什么是Skill.md文件

Skill.md文件是一种给AI提供"工作手册"的方式。就像你入职新公司时拿到的《开发规范文档》,Skill.md告诉AI:

  • 你们项目怎么组织代码
  • 有哪些约定俗成的规矩
  • 常见业务怎么处理
  • 代码写成什么样才算合格

核心价值:一次配置,持续复用

不用每次都重复说"我用React 18"、"组件要写在components文件夹",AI读了Skill.md就全懂了。


二、Skill.md vs Prompt的区别

对比维度 Prompt Skill.md
使用时机 每次对话都要写 写一次,长期使用
内容性质 具体任务描述 通用规范和知识
适用场景 "帮我生成一个登录页" "我们项目所有页面的开发规范"
更新频率 随需求变化 技术栈变更时才改
典型长度 100-500字 1000-5000字

举例说明

Prompt(任务型)

markdown
帮我写一个用户列表页,要有搜索、分页、删除功能
Skill.md(规范型)
markdown
# 项目开发规范

## 技术栈
React 18 + TypeScript + Ant Design

## 列表页开发规范
- 使用antd的Table组件
- 分页参数统一用pageNum和pageSize
- 删除操作必须二次确认
- 表格loading状态用全局store管理

有了Skill.md,你只需说"按规范写个用户列表页",AI就知道该怎么做。


三、实战案例一:表单组件批量开发

业务背景

你在开发一个ERP系统,需要做30+个表单页面:

  • 客户信息录入表单
  • 订单创建表单
  • 库存盘点表单
  • 财务报销表单
  • ...

这些表单有80%的逻辑是重复的:

  • 字段校验规则类似
  • 提交失败提示一样
  • loading状态处理一样
  • 布局风格统一

如果每个表单都从头写,累死也写不完。

解决方案:form-generator.skill.md

创建一个专门生成表单的Skill文件,把所有通用规范写进去。

完整Skill文件内容

markdown
# Skill: 表单组件生成器

## 使用场景

当需要创建任何数据录入表单时使用此Skill,包括但不限于:
- 用户信息表单
- 订单创建表单
- 设置配置表单
- 筛选搜索表单

## 技术栈约定

### 核心依赖
- React 18.2
- TypeScript 5.0
- Ant Design 5.x
- react-hook-form 7.x(表单状态管理)
- zod(表单校验)

### 状态管理
- 表单内部状态:react-hook-form
- 全局loading状态:Zustand的useLoadingStore
- 提交成功后刷新:React Query的invalidateQueries

## 代码规范

### 1. 文件结构

每个表单组件必须按以下结构组织:
markdown
### 2. 命名规范

- 组件名:必须以Form结尾,如UserInfoForm、OrderCreateForm
- 类型名:组件Props用FormNameProps,表单数据用FormNameValues
- Hook名:用use开头,如useUserFormSubmit
- 常量:全大写下划线分隔,如DEFAULT_FORM_VALUES

### 3. 类型定义模板

```typescript
// types.ts
import { z } from 'zod';
import { formSchema } from './schema';

// 从schema推导类型
export type FormValues = z.infer<typeof formSchema>;

// 组件Props
export interface FormNameProps {
  // 表单模式:create新建 | edit编辑 | view查看
  mode: 'create' | 'edit' | 'view';
  // 编辑模式必传初始数据
  initialData?: FormValues;
  // 提交成功回调
  onSuccess?: (data: FormValues) => void;
  // 取消回调
  onCancel?: () => void;
}
```text

### 4. Schema定义规范

使用Zod定义校验规则,必须包含中文错误提示:

```typescript
// schema.ts
import { z } from 'zod';

export const formSchema = z.object({
  // 必填字段
  username: z.string()
    .min(1, '请输入用户名')
    .max(20, '用户名不能超过20个字符'),

  // 邮箱校验
  email: z.string()
    .email('请输入正确的邮箱格式'),

  // 手机号校验
  phone: z.string()
    .regex(/^1[3-9]\d{9}$/, '请输入正确的手机号'),

  // 数字范围
  age: z.number()
    .min(18, '年龄不能小于18岁')
    .max(100, '请输入正确的年龄'),

  // 可选字段
  remark: z.string().optional(),
});
```text

### 5. 组件结构模板

```typescript
// index.tsx
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Form, Input, Button, Space } from 'antd';
import { formSchema } from './schema';
import { FormNameProps, FormValues } from './types';
import { useFormSubmit } from './hooks/useFormSubmit';

/**
 * 【组件功能描述】
 * @param mode - 表单模式
 * @param initialData - 初始数据(编辑模式)
 * @param onSuccess - 提交成功回调
 * @param onCancel - 取消回调
 */
export default function FormName({
  mode,
  initialData,
  onSuccess,
  onCancel,
}: FormNameProps) {
  // 表单实例
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: initialData,
  });

  // 提交逻辑
  const { submitForm } = useFormSubmit({ mode, onSuccess });

  // 是否只读模式
  const isReadOnly = mode === 'view';

  return (
    <Form layout="vertical">
      {/* 表单字段按业务逻辑分组 */}

      {/* 操作按钮 */}
      {!isReadOnly && (
        <Space>
          <Button
            type="primary"
            loading={isSubmitting}
            onClick={handleSubmit(submitForm)}
          >
            {mode === 'create' ? '创建' : '保存'}
          </Button>
          <Button onClick={onCancel}>取消</Button>
        </Space>
      )}
    </Form>
  );
}
```text

### 6. 提交逻辑Hook模板

```typescript
// hooks/useFormSubmit.ts
import { message } from 'antd';
import { useMutation } from '@tanstack/react-query';
import { FormValues } from '../types';
import { createAPI, updateAPI } from '@/api/xxx';

interface UseFormSubmitProps {
  mode: 'create' | 'edit' | 'view';
  onSuccess?: (data: FormValues) => void;
}

export function useFormSubmit({ mode, onSuccess }: UseFormSubmitProps) {
  // 创建mutation
  const { mutateAsync: createMutate } = useMutation({
    mutationFn: createAPI,
  });

  const { mutateAsync: updateMutate } = useMutation({
    mutationFn: updateAPI,
  });

  // 提交函数
  const submitForm = async (data: FormValues) => {
    try {
      if (mode === 'create') {
        await createMutate(data);
        message.success('创建成功');
      } else {
        await updateMutate(data);
        message.success('保存成功');
      }
      onSuccess?.(data);
    } catch (error) {
      message.error('操作失败,请重试');
      console.error('Form submit error:', error);
    }
  };

  return { submitForm };
}
```text

## 业务逻辑处理规范

### 1. 字段联动

当一个字段变化影响其他字段时:

```typescript
// 使用watch监听字段变化
const categoryId = watch('categoryId');

useEffect(() => {
  if (categoryId) {
    // 联动更新子分类选项
    setValue('subCategoryId', undefined);
  }
}, [categoryId]);
```text

### 2. 动态表单(数组字段)

```typescript
import { useFieldArray } from 'react-hook-form';

const { fields, append, remove } = useFieldArray({
  control,
  name: 'items', // 数组字段名
});

// 渲染
{fields.map((field, index) => (
  <div key={field.id}>
    <Input {...register(`items.${index}.name`)} />
    <Button onClick={() => remove(index)}>删除</Button>
  </div>
))}

<Button onClick={() => append({ name: '', qty: 0 })}>
  添加项目
</Button>
```text

### 3. 文件上传

```typescript
// 使用Ant Design Upload组件
<Upload
  beforeUpload={(file) => {
    // 校验文件
    const isLt2M = file.size / 1024 / 1024 < 2;
    if (!isLt2M) {
      message.error('文件不能超过2MB');
      return false;
    }
    return true;
  }}
  onChange={(info) => {
    if (info.file.status === 'done') {
      setValue('fileUrl', info.file.response.url);
    }
  }}
>
  <Button>点击上传</Button>
</Upload>
```text

### 4. 日期处理

```typescript
import dayjs from 'dayjs';

// 提交前格式化
const onSubmit = (data: FormValues) => {
  const formatted = {
    ...data,
    birthDate: dayjs(data.birthDate).format('YYYY-MM-DD'),
  };
  submitForm(formatted);
};
```text

## 错误处理规范

### 1. 字段校验错误

自动显示在字段下方,使用Ant Design的Form.Item:

```typescript
<Form.Item
  label="用户名"
  validateStatus={errors.username ? 'error' : ''}
  help={errors.username?.message}
>
  <Input {...register('username')} />
</Form.Item>
```text

### 2. 提交失败处理

```typescript
try {
  await submitForm(data);
} catch (error) {
  // 后端返回字段级错误
  if (error.response?.data?.fieldErrors) {
    Object.entries(error.response.data.fieldErrors).forEach(
      ([field, msg]) => {
        setError(field as keyof FormValues, {
          message: msg as string,
        });
      }
    );
  } else {
    // 通用错误提示
    message.error('操作失败,请稍后重试');
  }
}
```text

## 常用表单字段配置

### 文本输入框

```typescript
<Form.Item label="姓名">
  <Input
    placeholder="请输入姓名"
    maxLength={50}
    {...register('name')}
  />
</Form.Item>
```text

### 数字输入框

```typescript
<Form.Item label="年龄">
  <InputNumber
    min={0}
    max={150}
    {...register('age', { valueAsNumber: true })}
  />
</Form.Item>
```text

### 下拉选择

```typescript
<Form.Item label="部门">
  <Select
    placeholder="请选择部门"
    options={DEPARTMENT_OPTIONS}
    {...register('departmentId')}
  />
</Form.Item>
```text

### 单选按钮

```typescript
<Form.Item label="性别">
  <Radio.Group {...register('gender')}>
    <Radio value="male">男</Radio>
    <Radio value="female">女</Radio>
  </Radio.Group>
</Form.Item>
```text

### 多选框

```typescript
<Form.Item label="兴趣爱好">
  <Checkbox.Group
    options={HOBBY_OPTIONS}
    {...register('hobbies')}
  />
</Form.Item>
```text

### 日期选择

```typescript
<Form.Item label="出生日期">
  <DatePicker
    format="YYYY-MM-DD"
    {...register('birthDate')}
  />
</Form.Item>
```text

### 多行文本

```typescript
<Form.Item label="备注">
  <Input.TextArea
    rows={4}
    maxLength={500}
    showCount
    {...register('remark')}
  />
</Form.Item>
```text

## 输出要求

当用户要求生成表单时,必须:

1. 生成完整的文件结构(所有必需文件)
2. 类型定义完整,无any类型
3. 包含所有字段的校验规则
4. 提交逻辑完整(loading、错误处理)
5. 至少3个测试用例覆盖:
    - 表单校验测试
    - 成功提交测试
    - 失败提交测试

## 测试用例模板

```typescript
// __tests__/index.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import FormName from '../index';

describe('FormName', () => {
  it('创建模式:必填字段为空时显示错误', async () => {
    render(<FormName mode="create" />);

    const submitBtn = screen.getByText('创建');
    fireEvent.click(submitBtn);

    await waitFor(() => {
      expect(screen.getByText('请输入用户名')).toBeInTheDocument();
    });
  });

  it('成功提交后调用onSuccess回调', async () => {
    const onSuccess = jest.fn();
    render(<FormName mode="create" onSuccess={onSuccess} />);

    // 填写表单...

    const submitBtn = screen.getByText('创建');
    fireEvent.click(submitBtn);

    await waitFor(() => {
      expect(onSuccess).toHaveBeenCalled();
    });
  });

  it('查看模式:表单字段只读', () => {
    render(<FormName mode="view" initialData={mockData} />);

    const inputs = screen.getAllByRole('textbox');
    inputs.forEach(input => {
      expect(input).toBeDisabled();
    });
  });
});
```text

## 使用示例

### 用户输入

```plain
请按照form-generator规范,生成一个客户信息录入表单。

字段包括:
- 客户名称(必填,最多50字)
- 联系人(必填)
- 手机号(必填,11位)
- 邮箱(选填)
- 公司地址(选填)
- 客户类型(单选:个人/企业)
- 备注(选填,最多200字)
```text

### AI输出

AI会自动生成:

1. 完整的文件结构(6个文件)
2. 符合规范的类型定义
3. Zod校验规则(中文错误提示)
4. 标准的组件代码
5. 提交逻辑hook
6. 3个测试用例

你只需要:

- 复制代码到项目
- 补充真实的API接口
- 跑一下测试
- 基本上不用改就能用

---

## 四、实战案例二:CRUD业务模板

### 业务背景

后台管理系统最常见的工作:列表页 + 详情页 + 编辑页。

每个业务对象(用户、订单、商品、文章...)都要写这三个页面,代码重复度极高:

- 列表页:表格 + 搜索 + 分页 + 新建/编辑/删除按钮
- 详情页:展示信息,字段只读
- 编辑页:表单,可修改并保存

### 解决方案:crud-template.skill.md

#### 完整Skill文件

```markdown
## Skill: CRUD业务模板生成器

## 使用场景

用于快速生成标准的列表-详情-编辑三页面结构,适用于:
- 用户管理
- 订单管理
- 商品管理
- 内容管理
- 任何需要CRUD操作的业务对象

## 技术栈

### 核心依赖
- React 18 + TypeScript
- React Router v6(路由)
- @tanstack/react-query(数据请求)
- Zustand(全局状态)
- Ant Design 5.x(UI组件)

### 状态管理分工
- 列表数据:React Query(支持缓存和自动刷新)
- 筛选条件:URL Query参数(支持刷新保持状态)
- 弹窗开关:组件内useState
- 用户信息:Zustand全局store

## 文件结构规范

每个CRUD模块必须按以下结构组织:
```text

ModuleName/ ├── index.tsx # 路由入口 ├── List/ │ ├── index.tsx # 列表页主组件 │ ├── SearchForm.tsx # 搜索筛选表单 │ ├── columns.tsx # 表格列配置 │ ├── useListData.ts # 列表数据hook │ └── **tests**/ ├── Detail/ │ ├── index.tsx # 详情页 │ ├── InfoCard.tsx # 信息展示卡片 │ └── useDetailData.ts # 详情数据hook ├── Edit/ │ ├── index.tsx # 编辑页(新建和编辑共用) │ ├── EditForm.tsx # 表单组件 │ └── useEditSubmit.ts # 提交逻辑hook ├── types.ts # 模块类型定义 ├── api.ts # API请求函数 └── constants.ts # 常量配置

```markdown
## API层规范

### 1. API函数命名

```typescript
// api.ts
import request from '@/utils/request';
import { ListParams, ListResponse, DetailData, CreateParams, UpdateParams } from './types';

/**
 * 获取列表
 */
export async function fetchList(params: ListParams): Promise<ListResponse> {
  return request.get('/api/module/list', { params });
}

/**
 * 获取详情
 */
export async function fetchDetail(id: string): Promise<DetailData> {
  return request.get(`/api/module/${id}`);
}

/**
 * 创建
 */
export async function create(data: CreateParams): Promise<void> {
  return request.post('/api/module', data);
}

/**
 * 更新
 */
export async function update(id: string, data: UpdateParams): Promise<void> {
  return request.put(`/api/module/${id}`, data);
}

/**
 * 删除
 */
export async function remove(id: string): Promise<void> {
  return request.delete(`/api/module/${id}`);
}

2. 类型定义

typescript
// types.ts

// 列表查询参数
export interface ListParams {
  pageNum: number;
  pageSize: number;
  keyword?: string;
  status?: string;
  startDate?: string;
  endDate?: string;
  // 其他筛选字段...
}

// 列表响应
export interface ListResponse {
  list: ListItem[];
  total: number;
}

// 列表项
export interface ListItem {
  id: string;
  name: string;
  status: 'active' | 'inactive';
  createTime: string;
  updateTime: string;
  // 其他字段...
}

// 详情数据(通常比列表项字段更多)
export interface DetailData extends ListItem {
  description: string;
  // 更多详细字段...
}

// 创建参数
export interface CreateParams {
  name: string;
  // 其他必填字段...
}

// 更新参数(通常和创建参数一样,但某些字段可选)
export interface UpdateParams extends Partial<CreateParams> {}

列表页规范

1. 筛选条件管理

使用URL Query参数存储筛选条件,好处:

  • 刷新页面筛选条件不丢失
  • 可以分享筛选后的URL给同事
  • 浏览器前进后退按钮可用
typescript
// List/useListData.ts
import { useSearchParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { fetchList } from '../api';

export function useListData() {
  const [searchParams, setSearchParams] = useSearchParams();

  // 从URL读取筛选条件
  const params = {
    pageNum: Number(searchParams.get('pageNum')) || 1,
    pageSize: Number(searchParams.get('pageSize')) || 20,
    keyword: searchParams.get('keyword') || '',
    status: searchParams.get('status') || '',
  };

  // 获取列表数据
  const { data, isLoading, refetch } = useQuery({
    queryKey: ['moduleList', params],
    queryFn: () => fetchList(params),
  });

  // 更新筛选条件
  const updateParams = (newParams: Partial<typeof params>) => {
    const updated = { ...params, ...newParams };
    // 筛选条件变化时,重置到第一页
    if (newParams.keyword !== undefined || newParams.status !== undefined) {
      updated.pageNum = 1;
    }
    setSearchParams(updated as any);
  };

  return {
    list: data?.list || [],
    total: data?.total || 0,
    params,
    isLoading,
    updateParams,
    refetch,
  };
}

2. 搜索表单组件

typescript
// List/SearchForm.tsx
import { Form, Input, Select, Button, Space } from 'antd';

interface SearchFormProps {
  initialValues: {
    keyword?: string;
    status?: string;
  };
  onSearch: (values: any) => void;
}

export default function SearchForm({ initialValues, onSearch }: SearchFormProps) {
  const [form] = Form.useForm();

  const handleReset = () => {
    form.resetFields();
    onSearch({});
  };

  return (
    <Form
      form={form}
      layout="inline"
      initialValues={initialValues}
      onFinish={onSearch}
    >
      <Form.Item name="keyword">
        <Input placeholder="搜索关键词" style={{ width: 200 }} />
      </Form.Item>

      <Form.Item name="status">
        <Select
          placeholder="状态"
          style={{ width: 120 }}
          options={[
            { label: '全部', value: '' },
            { label: '启用', value: 'active' },
            { label: '禁用', value: 'inactive' },
          ]}
        />
      </Form.Item>

      <Form.Item>
        <Space>
          <Button type="primary" htmlType="submit">
            搜索
          </Button>
          <Button onClick={handleReset}>
            重置
          </Button>
        </Space>
      </Form.Item>
    </Form>
  );
}

3. 表格列配置

typescript
// List/columns.tsx
import { Space, Button, Popconfirm, Tag } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { ListItem } from '../types';

interface GetColumnsParams {
  onEdit: (record: ListItem) => void;
  onDelete: (id: string) => void;
  onDetail: (id: string) => void;
}

export function getColumns({ onEdit, onDelete, onDetail }: GetColumnsParams): ColumnsType<ListItem> {
  return [
    {
      title: 'ID',
      dataIndex: 'id',
      width: 100,
    },
    {
      title: '名称',
      dataIndex: 'name',
      width: 200,
    },
    {
      title: '状态',
      dataIndex: 'status',
      width: 100,
      render: (status: string) => (
        <Tag color={status === 'active' ? 'green' : 'red'}>
          {status === 'active' ? '启用' : '禁用'}
        </Tag>
      ),
    },
    {
      title: '创建时间',
      dataIndex: 'createTime',
      width: 180,
    },
    {
      title: '操作',
      key: 'action',
      width: 200,
      fixed: 'right',
      render: (_, record) => (
        <Space>
          <Button type="link" size="small" onClick={() => onDetail(record.id)}>
            详情
          </Button>
          <Button type="link" size="small" onClick={() => onEdit(record)}>
            编辑
          </Button>
          <Popconfirm
            title="确认删除?"
            onConfirm={() => onDelete(record.id)}
            okText="确认"
            cancelText="取消"
          >
            <Button type="link" size="small" danger>
              删除
            </Button>
          </Popconfirm>
        </Space>
      ),
    },
  ];
}

4. 列表页主组件

typescript
// List/index.tsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Table, Button, message, Modal } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import SearchForm from './SearchForm';
import { getColumns } from './columns';
import { useListData } from './useListData';
import { remove } from '../api';
import EditForm from '../Edit/EditForm';

export default function List() {
  const navigate = useNavigate();
  const { list, total, params, isLoading, updateParams, refetch } = useListData();

  const [editModalOpen, setEditModalOpen] = useState(false);
  const [editingRecord, setEditingRecord] = useState<any>(null);

  // 搜索
  const handleSearch = (values: any) => {
    updateParams(values);
  };

  // 分页
  const handlePageChange = (pageNum: number, pageSize: number) => {
    updateParams({ pageNum, pageSize });
  };

  // 新建
  const handleCreate = () => {
    setEditingRecord(null);
    setEditModalOpen(true);
  };

  // 编辑
  const handleEdit = (record: any) => {
    setEditingRecord(record);
    setEditModalOpen(true);
  };

  // 删除
  const handleDelete = async (id: string) => {
    try {
      await remove(id);
      message.success('删除成功');
      refetch();
    } catch (error) {
      message.error('删除失败');
    }
  };

  // 查看详情
  const handleDetail = (id: string) => {
    navigate(`/module/${id}`);
  };

  // 编辑成功
  const handleEditSuccess = () => {
    setEditModalOpen(false);
    refetch();
  };

  const columns = getColumns({
    onEdit: handleEdit,
    onDelete: handleDelete,
    onDetail: handleDetail,
  });

  return (
    <div>
      <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
        <SearchForm
          initialValues={{
            keyword: params.keyword,
            status: params.status,
          }}
          onSearch={handleSearch}
        />
        <Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
          新建
        </Button>
      </div>

      <Table
        rowKey="id"
        columns={columns}
        dataSource={list}
        loading={isLoading}
        pagination={{
          current: params.pageNum,
          pageSize: params.pageSize,
          total,
          showSizeChanger: true,
          showTotal: (total) => `共 ${total} 条`,
          onChange: handlePageChange,
        }}
      />

      <Modal
        title={editingRecord ? '编辑' : '新建'}
        open={editModalOpen}
        onCancel={() => setEditModalOpen(false)}
        footer={null}
        width={600}
      >
        <EditForm
          mode={editingRecord ? 'edit' : 'create'}
          initialData={editingRecord}
          onSuccess={handleEditSuccess}
          onCancel={() => setEditModalOpen(false)}
        />
      </Modal>
    </div>
  );
}

详情页规范

typescript
// Detail/index.tsx
import { useParams, useNavigate } from 'react-router-dom';
import { Card, Descriptions, Button, Spin } from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { useQuery } from '@tanstack/react-query';
import { fetchDetail } from '../api';

export default function Detail() {
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();

  const { data, isLoading } = useQuery({
    queryKey: ['moduleDetail', id],
    queryFn: () => fetchDetail(id!),
    enabled: !!id,
  });

  if (isLoading) {
    return <Spin />;
  }

  return (
    <div>
      <Button
        icon={<ArrowLeftOutlined />}
        onClick={() => navigate(-1)}
        style={{ marginBottom: 16 }}
      >
        返回
      </Button>

      <Card title="基本信息">
        <Descriptions column={2}>
          <Descriptions.Item label="ID">{data?.id}</Descriptions.Item>
          <Descriptions.Item label="名称">{data?.name}</Descriptions.Item>
          <Descriptions.Item label="状态">{data?.status}</Descriptions.Item>
          <Descriptions.Item label="创建时间">{data?.createTime}</Descriptions.Item>
          <Descriptions.Item label="描述" span={2}>
            {data?.description}
          </Descriptions.Item>
        </Descriptions>
      </Card>
    </div>
  );
}

编辑页规范

编辑页和新建页共用一个组件,通过mode区分:

typescript
// Edit/EditForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Form, Input, Button, Space, message } from 'antd';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { create, update } from '../api';
import { editSchema } from '../schema';

interface EditFormProps {
  mode: 'create' | 'edit';
  initialData?: any;
  onSuccess?: () => void;
  onCancel?: () => void;
}

export default function EditForm({ mode, initialData, onSuccess, onCancel }: EditFormProps) {
  const queryClient = useQueryClient();

  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm({
    resolver: zodResolver(editSchema),
    defaultValues: initialData,
  });

  // 创建mutation
  const createMutation = useMutation({
    mutationFn: create,
    onSuccess: () => {
      message.success('创建成功');
      queryClient.invalidateQueries({ queryKey: ['moduleList'] });
      onSuccess?.();
    },
  });

  // 更新mutation
  const updateMutation = useMutation({
    mutationFn: ({ id, data }: any) => update(id, data),
    onSuccess: () => {
      message.success('保存成功');
      queryClient.invalidateQueries({ queryKey: ['moduleList'] });
      queryClient.invalidateQueries({ queryKey: ['moduleDetail'] });
      onSuccess?.();
    },
  });

  const onSubmit = async (data: any) => {
    try {
      if (mode === 'create') {
        await createMutation.mutateAsync(data);
      } else {
        await updateMutation.mutateAsync({ id: initialData.id, data });
      }
    } catch (error) {
      message.error('操作失败');
    }
  };

  return (
    <Form layout="vertical" onFinish={handleSubmit(onSubmit)}>
      <Form.Item
        label="名称"
        validateStatus={errors.name ? 'error' : ''}
        help={errors.name?.message as string}
      >
        <Input {...register('name')} placeholder="请输入名称" />
      </Form.Item>

      {/* 其他字段... */}

      <Form.Item>
        <Space>
          <Button type="primary" htmlType="submit" loading={isSubmitting}>
            {mode === 'create' ? '创建' : '保存'}
          </Button>
          <Button onClick={onCancel}>取消</Button>
        </Space>
      </Form.Item>
    </Form>
  );
}

路由配置

typescript
// index.tsx
import { Routes, Route } from 'react-router-dom';
import List from './List';
import Detail from './Detail';

export default function ModuleRoutes() {
  return (
    <Routes>
      <Route path="/" element={<List />} />
      <Route path="/:id" element={<Detail />} />
    </Routes>
  );
}

数据刷新策略

使用React Query的自动刷新机制:

typescript
// 列表查询配置
useQuery({
  queryKey: ['moduleList', params],
  queryFn: () => fetchList(params),
  staleTime: 5 * 60 * 1000, // 5分钟内认为数据是新鲜的
  refetchOnWindowFocus: true, // 窗口重新获得焦点时刷新
});

// 详情查询配置
useQuery({
  queryKey: ['moduleDetail', id],
  queryFn: () => fetchDetail(id),
  staleTime: 10 * 60 * 1000, // 10分钟
});

// 操作成功后手动刷新
queryClient.invalidateQueries({ queryKey: ['moduleList'] });

权限控制

typescript
// 使用自定义hook检查权限
import { usePermission } from '@/hooks/usePermission';

export default function List() {
  const { hasPermission } = usePermission();

  const canCreate = hasPermission('module:create');
  const canEdit = hasPermission('module:edit');
  const canDelete = hasPermission('module:delete');

  return (
    <div>
      {canCreate && (
        <Button onClick={handleCreate}>新建</Button>
      )}

      <Table
        columns={getColumns({
          onEdit: canEdit ? handleEdit : undefined,
          onDelete: canDelete ? handleDelete : undefined,
        })}
      />
    </div>
  );
}

输出要求

生成CRUD模块时必须:

  1. 包含完整文件结构(所有页面和hook)
  2. 列表页支持筛选、分页、刷新
  3. 筛选条件存储在URL中
  4. 使用React Query管理数据
  5. 编辑和新建共用表单组件
  6. 包含loading和错误处理
  7. 操作成功后自动刷新列表

使用示例

用户输入

plain
按CRUD模板生成一个商品管理模块。

列表页筛选条件:
- 商品名称搜索
- 分类筛选(单选下拉)
- 状态筛选(上架/下架)
- 价格区间筛选

列表展示字段:
- 商品ID
- 商品名称
- 分类
- 价格
- 库存
- 状态
- 创建时间

编辑表单字段:
- 商品名称(必填)
- 分类(必选)
- 价格(必填,大于0)
- 库存(必填,整数)
- 商品描述(选填)
- 商品图片(上传)

AI输出

自动生成15+个文件,包括:

  • 列表页完整代码
  • 搜索表单(4个筛选项)
  • 详情页
  • 编辑表单(带图片上传)
  • 所有API函数
  • 所有TypeScript类型
  • React Query配置
  • 路由配置

复制粘贴到项目,补充真实API,基本可用。


五、实战案例三:代码规范自动化

业务背景

团队10个人,每个人代码风格不一样:

  • 有人喜欢用function,有人喜欢箭头函数
  • 变量命名五花八门
  • 注释有的写有的不写
  • 错误处理方式各异

Code Review时吵得不可开交,浪费大量时间。

解决方案:code-standard.skill.md

定义团队统一的代码规范,AI生成的代码自动符合规范。

完整Skill文件

markdown
# Skill: 代码规范标准

## 适用范围

本规范适用于项目所有前端代码,包括:
- React组件
- TypeScript类型定义
- 工具函数
- API请求
- 测试代码

## 命名规范

### 1. 文件命名

- **组件文件**:PascalCase.tsx
  - 示例:`UserCard.tsx`、`ProductList.tsx`
- **工具函数文件**:camelCase.ts
  - 示例:`formatDate.ts`、`validateEmail.ts`
- **常量文件**:camelCase.ts或UPPER_SNAKE_CASE.ts
  - 示例:`apiConfig.ts`、`APP_CONFIG.ts`
- **类型文件**:camelCase.types.ts
  - 示例:`user.types.ts`、`api.types.ts`
- **Hook文件**:useXxx.ts
  - 示例:`useUserData.ts`、`useDebounce.ts`

### 2. 变量命名

- **常量**:UPPER_SNAKE_CASE
  ```typescript
  const MAX_RETRY_COUNT = 3;
  const API_BASE_URL = 'https://api.example.com';
```text

- **普通变量**:camelCase

```typescript
const userName = 'Alice';
const productList = [];
```text

- **布尔值**:用is/has/should开头

```typescript
const isLoading = false;
const hasPermission = true;
const shouldShowModal = false;
```text

- **数组**:用复数形式

```typescript
const users = [];
const products = [];
```text

- **对象**:用单数形式

```typescript
const user = { id: 1, name: 'Alice' };
const config = { timeout: 3000 };
```text

### 3. 函数命名

- **普通函数**:camelCase,动词开头

```typescript
function fetchUserData() {}
function validateEmail() {}
function calculateTotal() {}
```text

- **事件处理函数**:handle开头

```typescript
function handleClick() {}
function handleSubmit() {}
function handleInputChange() {}
```text

- **React组件**:PascalCase

```typescript
function UserCard() {}
function ProductList() {}
```text

- **自定义Hook**:use开头,camelCase

```typescript
function useUserData() {}
function useDebounce() {}
```text

### 4. 类型命名

- **Interface**:PascalCase,不加I前缀

```typescript
interface User {
  id: string;
  name: string;
}
```text

- **Type**:PascalCase

```typescript
type Status = 'pending' | 'success' | 'error';
```text

- **泛型参数**:单个大写字母或PascalCase

```typescript
function identity<T>(arg: T): T {}
function map<TInput, TOutput>(fn: (item: TInput) => TOutput) {}
```text

## 目录结构规范

```plain
src/
├── components/          # 公共组件
│   ├── Button/
│   │   ├── index.tsx
│   │   ├── Button.types.ts
│   │   └── Button.module.css
│   └── ...
├── pages/               # 页面组件
│   ├── User/
│   │   ├── List/
│   │   ├── Detail/
│   │   └── Edit/
│   └── ...
├── hooks/               # 自定义Hooks
├── utils/               # 工具函数
├── services/            # API服务
├── stores/              # 状态管理
├── types/               # 全局类型定义
├── constants/           # 常量配置
└── assets/              # 静态资源
```text

## 代码风格规范

### 1. 函数定义

**统一使用箭头函数(除了React组件)**

```typescript
// ✅ 推荐
const fetchUserData = async (id: string) => {
  // ...
};

// ❌ 不推荐
async function fetchUserData(id: string) {
  // ...
}

// ✅ React组件用function
export default function UserCard() {
  return <div>...</div>;
}
```text

### 2. 解构赋值

**能解构的尽量解构**

```typescript
// ✅ 推荐
const { name, age } = user;
const [first, second, ...rest] = array;

// ❌ 不推荐
const name = user.name;
const age = user.age;
```text

### 3. 可选链和空值合并

**使用?.和??简化代码**

```typescript
// ✅ 推荐
const userName = user?.profile?.name ?? '匿名用户';

// ❌ 不推荐
const userName = user && user.profile && user.profile.name || '匿名用户';
```text

### 4. 模板字符串

**字符串拼接用模板字符串**

```typescript
// ✅ 推荐
const greeting = `Hello, ${userName}!`;

// ❌ 不推荐
const greeting = 'Hello, ' + userName + '!';
```text

## 注释规范

### 1. JSDoc注释

**所有导出的函数和组件必须有JSDoc注释**

```typescript
/**
 * 格式化日期
 * @param date - 日期对象或时间戳
 * @param format - 格式化模板,默认'YYYY-MM-DD'
 * @returns 格式化后的日期字符串
 * @example
 * formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
 * // => '2024-01-01 12:00:00'
 */
export const formatDate = (
  date: Date | number,
  format: string = 'YYYY-MM-DD'
): string => {
  // 实现...
};
```text

### 2. 行内注释

**复杂逻辑必须加注释说明**

```typescript
// ✅ 推荐
// 计算折扣:满100减10,满200减30
const discount = total >= 200 ? 30 : total >= 100 ? 10 : 0;

// ❌ 不推荐(没注释,看不懂)
const discount = total >= 200 ? 30 : total >= 100 ? 10 : 0;
```text

### 3. TODO注释

**临时方案用TODO标记**

```typescript
// TODO: 优化性能,改用虚拟滚动
const renderList = () => {
  return items.map(item => <Item key={item.id} data={item} />);
};

// FIXME: 修复Safari浏览器兼容问题
const handleScroll = () => {
  // ...
};
```text

## TypeScript规范

### 1. 禁止使用any

**必须指定明确类型,特殊情况用unknown**

```typescript
// ✅ 推荐
const fetchData = async (url: string): Promise<User[]> => {
  // ...
};

// ❌ 禁止
const fetchData = async (url: string): Promise<any> => {
  // ...
};

// ✅ 不确定类型时用unknown
const parseJSON = (json: string): unknown => {
  return JSON.parse(json);
};
```text

### 2. 类型定义要完整

**Props、返回值、参数都要定义类型**

```typescript
// ✅ 推荐
interface ButtonProps {
  text: string;
  onClick: () => void;
  disabled?: boolean;
}

export default function Button({ text, onClick, disabled = false }: ButtonProps) {
  return <button onClick={onClick} disabled={disabled}>{text}</button>;
}

// ❌ 不推荐
export default function Button({ text, onClick, disabled }) {
  // 没有类型定义
}
```text

### 3. 用Type还是Interface?

- 组件Props、函数参数 → 用Interface
- 联合类型、工具类型 → 用Type

```typescript
// ✅ Interface用于对象结构
interface User {
  id: string;
  name: string;
}

// ✅ Type用于联合类型
type Status = 'pending' | 'success' | 'error';
type ButtonSize = 'small' | 'medium' | 'large';
```text

## 错误处理规范

### 1. 统一错误处理

**所有异步操作必须try-catch**

```typescript
// ✅ 推荐
const fetchUserData = async (id: string) => {
  try {
    const data = await api.getUser(id);
    return data;
  } catch (error) {
    console.error('获取用户数据失败:', error);
    message.error('获取数据失败,请稍后重试');
    throw error; // 继续抛出,让上层决定如何处理
  }
};
```text

### 2. 错误提示要具体

**告诉用户哪里出错了,怎么解决**

```typescript
// ✅ 推荐
if (!email.includes('@')) {
  throw new Error('邮箱格式不正确,请输入包含@的邮箱地址');
}

// ❌ 不推荐
if (!email.includes('@')) {
  throw new Error('输入错误');
}
```text

### 3. 网络请求错误处理

```typescript
const fetchData = async () => {
  try {
    const res = await api.getData();
    return res.data;
  } catch (error: any) {
    // 区分不同错误类型
    if (error.response?.status === 401) {
      message.error('登录已过期,请重新登录');
      // 跳转登录页
    } else if (error.response?.status === 403) {
      message.error('无权限访问');
    } else if (error.response?.status === 404) {
      message.error('请求的资源不存在');
    } else if (error.code === 'ECONNABORTED') {
      message.error('请求超时,请检查网络');
    } else {
      message.error('操作失败,请稍后重试');
    }
    throw error;
  }
};
```text

## React规范

### 1. 组件拆分原则

**单一职责,一个组件只做一件事**

```typescript
// ✅ 推荐:拆分成多个小组件
function UserList() {
  return (
    <div>
      <SearchBar />
      <UserTable />
      <Pagination />
    </div>
  );
}

// ❌ 不推荐:所有逻辑堆在一个组件
function UserList() {
  return (
    <div>
      {/* 500行代码... */}
    </div>
  );
}
```text

### 2. 状态管理

**能用props就不用state,能用local state就不用global state**

```typescript
// ✅ 推荐
function Modal({ open, onClose }) {
  // 弹窗开关由父组件控制
  return <div>{open && <div>...</div>}</div>;
}

// ❌ 不推荐
function Modal() {
  const [open, setOpen] = useState(false);
  // 弹窗状态应该由父组件控制,而不是自己管理
}
```text

### 3. useEffect规范

**依赖数组必须正确,避免无限循环**

```typescript
// ✅ 推荐
useEffect(() => {
  fetchData(userId);
}, [userId]); // 依赖明确

// ❌ 不推荐
useEffect(() => {
  fetchData(userId);
}); // 缺少依赖数组,每次渲染都执行
```text

### 4. 性能优化

**列表必须加key,避免不必要的re-render**

```typescript
// ✅ 推荐
{users.map(user => (
  <UserCard key={user.id} data={user} />
))}

// ❌ 不推荐
{users.map((user, index) => (
  <UserCard key={index} data={user} />
))}
```text

**大列表用React.memo**

```typescript
const UserCard = React.memo(({ data }: { data: User }) => {
  return <div>{data.name}</div>;
});
```text

## 代码质量检查清单

在提交代码前,AI和人工都要检查以下项:

- [ ] 所有导出函数有JSDoc注释
- [ ] 没有any类型
- [ ] Props有完整TypeScript定义
- [ ] 异步操作有try-catch
- [ ] 列表渲染有key
- [ ] useEffect依赖数组正确
- [ ] 文件命名符合规范
- [ ] 变量命名语义化
- [ ] 没有console.log(除了错误日志)
- [ ] 没有被注释掉的代码

## 输出要求

AI生成代码时,必须完全遵守以上规范,违反任何一条都需要重新生成。

Code Review时发现不符合规范的代码,必须打回修改。

```markdown
---

## 六、实战案例四:UI组件库适配

### 业务背景

公司用Ant Design,但设计师给的UI稿和Ant Design默认样式差别很大:
- 主题色不一样
- 按钮圆角不一样
- 间距和尺寸不一样

需要二次封装所有组件,统一风格。

### 解决方案:component-wrapper.skill.md

#### Skill文件大纲

```markdown
# Skill: UI组件封装规范

## 设计Token配置
- 主题色定义
- 尺寸体系(small/medium/large对应的px值)
- 间距规范(4px基准)
- 圆角规范
- 字体规范

## 组件封装原则
- 保持Ant Design的API不变
- 只覆盖样式,不改逻辑
- 增加业务通用功能(权限控制、埋点等)

## 常见组件封装示例
- Button(主题色、圆角、埋点)
- Input(错误提示、字数统计)
- Table(空状态、loading样式)
- Modal(固定宽度、关闭确认)

## 使用示例
如何在项目中统一引入封装后的组件

(具体内容类似前面的完整展开方式)


七、如何编写自己的Skill文件

Step 1:识别重复性工作

花一周时间记录:

  • 哪些代码每次都差不多
  • 哪些规范总是要重复说明
  • 哪些坑经常踩

Step 2:提炼标准化规范

把重复的东西抽象成规则:

  • 技术栈固定吗?
  • 文件结构能统一吗?
  • 有通用的业务逻辑吗?

Step 3:编写Skill.md

参考本章提供的模板结构:

markdown
# Skill名称

## 使用场景
## 技术栈约定
## 代码规范
## 业务逻辑模板
## 常见场景处理
## 输出要求

Step 4:测试与迭代

  1. 用AI生成代码
  2. 检查哪里不符合预期
  3. 补充到Skill文件
  4. 重新生成,直到满意

Step 5:团队推广

  • 放到Git仓库
  • 培训团队成员
  • Code Review时检查是否符合Skill规范

八、Skill文件管理

版本控制

Skill文件也要git管理:

bash
project-root/
├── .skills/
│   ├── form-generator.skill.md
│   ├── crud-template.skill.md
│   ├── code-standard.skill.md
│   └── README.md

每次修改记录changelog:

markdown
# Changelog

## v2.0.0 - 2024-02-10
- 增加动态表单支持
- 优化错误处理

## v1.1.0 - 2024-01-15
- 补充文件上传规范
- 修正TypeScript类型定义

## v1.0.0 - 2024-01-01
- 初始版本

团队共享

方式1:Git仓库

bash
git clone https://github.com/team/skills.git

方式2:内部文档平台(Confluence/语雀)

方式3:脚手架集成

bash
npx create-app --with-skills

九、效果验证

对比数据收集

指标 改造前 改造后 提升
表单开发时间 2小时 15分钟 87.5%
CRUD页面开发 1天 2小时 75%
Code Review时间 1小时/次 20分钟/次 67%
代码规范一致性 60% 95% +35%

质量指标

用SonarQube扫描代码:

  • 代码重复率下降
  • 代码复杂度降低
  • 测试覆盖率提升

团队反馈

定期收集:

  • Skill文件是否好用
  • 还缺哪些场景覆盖
  • 规范是否合理

十、常见问题

Q1:Skill文件写得太详细,AI会不会受限?

不会。Skill是规范,不是限制。AI在遵守规范的前提下可以灵活发挥。

Q2:技术栈升级了,Skill文件要全部重写吗?

不用。只改变化的部分,比如React 17升级到18,只需要改useEffect的说明。

Q3:新人看不懂Skill文件怎么办?

加上使用示例和注释。Skill文件不只是给AI看的,也是团队文档。

Q4:多个Skill文件会不会冲突?

会。所以要明确Skill的适用场景,或者合并成一个大Skill。


十一、本章总结

Skill.md文件的核心价值:

  1. 一次编写,长期复用 - 不用每次都重复说规范
  2. 团队协作标准化 - 所有人用同一套规范,代码风格统一
  3. 效率成倍提升 - 重复性工作时间减少50%-90%
  4. 知识沉淀 - 把团队经验变成可执行的文档