一、什么是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(任务型)
帮我写一个用户列表页,要有搜索、分页、删除功能
Skill.md(规范型)
# 项目开发规范
## 技术栈
React 18 + TypeScript + Ant Design
## 列表页开发规范
- 使用antd的Table组件
- 分页参数统一用pageNum和pageSize
- 删除操作必须二次确认
- 表格loading状态用全局store管理
有了Skill.md,你只需说"按规范写个用户列表页",AI就知道该怎么做。
三、实战案例一:表单组件批量开发
业务背景
你在开发一个ERP系统,需要做30+个表单页面:
- 客户信息录入表单
- 订单创建表单
- 库存盘点表单
- 财务报销表单
- ...
这些表单有80%的逻辑是重复的:
- 字段校验规则类似
- 提交失败提示一样
- loading状态处理一样
- 布局风格统一
如果每个表单都从头写,累死也写不完。
解决方案:form-generator.skill.md
创建一个专门生成表单的Skill文件,把所有通用规范写进去。
完整Skill文件内容
# 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. 文件结构
每个表单组件必须按以下结构组织:
### 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. 类型定义
// 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给同事
- 浏览器前进后退按钮可用
// 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. 搜索表单组件
// 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. 表格列配置
// 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. 列表页主组件
// 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>
);
}
详情页规范
// 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区分:
// 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>
);
}
路由配置
// 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的自动刷新机制:
// 列表查询配置
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'] });
权限控制
// 使用自定义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模块时必须:
- 包含完整文件结构(所有页面和hook)
- 列表页支持筛选、分页、刷新
- 筛选条件存储在URL中
- 使用React Query管理数据
- 编辑和新建共用表单组件
- 包含loading和错误处理
- 操作成功后自动刷新列表
使用示例
用户输入
按CRUD模板生成一个商品管理模块。
列表页筛选条件:
- 商品名称搜索
- 分类筛选(单选下拉)
- 状态筛选(上架/下架)
- 价格区间筛选
列表展示字段:
- 商品ID
- 商品名称
- 分类
- 价格
- 库存
- 状态
- 创建时间
编辑表单字段:
- 商品名称(必填)
- 分类(必选)
- 价格(必填,大于0)
- 库存(必填,整数)
- 商品描述(选填)
- 商品图片(上传)
AI输出
自动生成15+个文件,包括:
- 列表页完整代码
- 搜索表单(4个筛选项)
- 详情页
- 编辑表单(带图片上传)
- 所有API函数
- 所有TypeScript类型
- React Query配置
- 路由配置
复制粘贴到项目,补充真实API,基本可用。
五、实战案例三:代码规范自动化
业务背景
团队10个人,每个人代码风格不一样:
- 有人喜欢用function,有人喜欢箭头函数
- 变量命名五花八门
- 注释有的写有的不写
- 错误处理方式各异
Code Review时吵得不可开交,浪费大量时间。
解决方案:code-standard.skill.md
定义团队统一的代码规范,AI生成的代码自动符合规范。
完整Skill文件
# 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
参考本章提供的模板结构:
# Skill名称
## 使用场景
## 技术栈约定
## 代码规范
## 业务逻辑模板
## 常见场景处理
## 输出要求
Step 4:测试与迭代
- 用AI生成代码
- 检查哪里不符合预期
- 补充到Skill文件
- 重新生成,直到满意
Step 5:团队推广
- 放到Git仓库
- 培训团队成员
- Code Review时检查是否符合Skill规范
八、Skill文件管理
版本控制
Skill文件也要git管理:
project-root/
├── .skills/
│ ├── form-generator.skill.md
│ ├── crud-template.skill.md
│ ├── code-standard.skill.md
│ └── README.md
每次修改记录changelog:
# Changelog
## v2.0.0 - 2024-02-10
- 增加动态表单支持
- 优化错误处理
## v1.1.0 - 2024-01-15
- 补充文件上传规范
- 修正TypeScript类型定义
## v1.0.0 - 2024-01-01
- 初始版本
团队共享
方式1:Git仓库
git clone https://github.com/team/skills.git
方式2:内部文档平台(Confluence/语雀)
方式3:脚手架集成
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文件的核心价值:
- 一次编写,长期复用 - 不用每次都重复说规范
- 团队协作标准化 - 所有人用同一套规范,代码风格统一
- 效率成倍提升 - 重复性工作时间减少50%-90%
- 知识沉淀 - 把团队经验变成可执行的文档