Kaynağa Gözat

Merge branch 'starter' of 139-template-116/d8d-vite-starter into starter

18617351030 5 ay önce
ebeveyn
işleme
3ea6a3a8a6
5 değiştirilmiş dosya ile 338 ekleme ve 67 silme
  1. 131 2
      .roo/rules/10-entity.md
  2. 11 63
      .roo/rules/11-entity-creation.md
  3. 45 0
      .roo/rules/13-ui-style.md
  4. 143 0
      README.md
  5. 8 2
      src/client/api.ts

+ 131 - 2
.roo/rules/10-entity.md

@@ -80,6 +80,8 @@ updatedAt!: Date;
 
 ## 7. Zod Schema 规范
 
+### 7.1 基础类型规范
+
 ```typescript
 export const EntitySchema = z.object({
   id: z.number().int().positive().openapi({ description: 'ID说明' }),
@@ -87,11 +89,11 @@ export const EntitySchema = z.object({
   fieldName: z.string()
     .max(255)
     .nullable()
-    .openapi({ 
+    .openapi({
       description: '字段说明',
       example: '示例值'
     }),
-  // 数字字段  
+  // 数字字段
   numberField: z.number()
     .default(默认值)
     .openapi({...}),
@@ -100,6 +102,133 @@ export const EntitySchema = z.object({
 });
 ```
 
+### 7.2 数据类型转换规范
+
+#### 7.2.1 数字类型转换
+
+对于URL参数或表单数据中的数字类型,必须使用`z.coerce.number()`进行类型转换,以确保字符串到数字的正确转换:
+
+```typescript
+// 整数类型
+z.coerce.number().int().positive().openapi({
+  description: '正整数ID',
+  example: 1
+});
+
+// 小数类型
+z.coerce.number().multipleOf(0.01).openapi({
+  description: '金额,保留两位小数',
+  example: 19.99
+});
+
+// 状态类型(0/1)
+z.coerce.number().int().min(0).max(1).openapi({
+  description: '状态(0-禁用,1-启用)',
+  example: 1
+});
+```
+
+#### 7.2.2 日期类型转换
+
+对于日期时间类型,必须使用`z.coerce.date()`进行类型转换:
+
+```typescript
+// 日期时间类型
+z.coerce.date().openapi({
+  description: '创建时间',
+  example: '2023-10-01T12:00:00Z'
+});
+
+// 日期范围查询
+const DateRangeSchema = z.object({
+  startDate: z.coerce.date().openapi({
+    description: '开始日期',
+    example: '2023-10-01T00:00:00Z'
+  }),
+  endDate: z.coerce.date().openapi({
+    description: '结束日期',
+    example: '2023-10-31T23:59:59Z'
+  })
+});
+```
+
+#### 7.2.3 布尔类型转换
+
+对于布尔类型参数,必须使用`z.coerce.boolean()`进行类型转换:
+
+```typescript
+// 布尔类型
+z.coerce.boolean().openapi({
+  description: '是否启用',
+  example: true
+});
+```
+
+### 7.3 创建/更新Schema特殊规范
+
+创建(Create)和更新(Update)Schema必须遵循以下额外规范:
+
+1. **创建Schema**:
+   - 不包含`id`字段(由数据库自动生成)
+   - 所有必填字段必须显式定义,不得为`optional()`
+   - 必须使用适当的coerce方法处理非字符串类型
+
+```typescript
+export const CreateEntityDto = z.object({
+  name: z.string().max(255).openapi({
+    description: '名称',
+    example: '示例名称'
+  }),
+  quantity: z.coerce.number().int().min(1).openapi({
+    description: '数量',
+    example: 10
+  }),
+  price: z.coerce.number().multipleOf(0.01).openapi({
+    description: '价格',
+    example: 99.99
+  }),
+  isActive: z.coerce.boolean().default(true).openapi({
+    description: '是否激活',
+    example: true
+  }),
+  expireDate: z.coerce.date().openapi({
+    description: '过期日期',
+    example: '2024-12-31T23:59:59Z'
+  })
+});
+```
+
+2. **更新Schema**:
+   - 不包含`id`字段(通过URL参数传递)
+   - 所有字段必须为`optional()`
+   - 必须使用适当的coerce方法处理非字符串类型
+
+```typescript
+export const UpdateEntityDto = z.object({
+  name: z.string().max(255).optional().openapi({
+    description: '名称',
+    example: '更新后的名称'
+  }),
+  quantity: z.coerce.number().int().min(1).optional().openapi({
+    description: '数量',
+    example: 20
+  }),
+  price: z.coerce.number().multipleOf(0.01).optional().openapi({
+    description: '价格',
+    example: 89.99
+  }),
+  isActive: z.coerce.boolean().optional().openapi({
+    description: '是否激活',
+    example: false
+  }),
+  expireDate: z.coerce.date().optional().openapi({
+    description: '过期日期',
+    example: '2025-12-31T23:59:59Z'
+  })
+});
+```
+```
+
 ## 8. 命名规范
 
 - 实体类名:PascalCase (如 RackInfo)

+ 11 - 63
.roo/rules/11-entity-creation.md

@@ -90,7 +90,7 @@
      
      export const yourEntityClient = hc<YourEntityRoutes>('/api/v1', {
        fetch: axiosFetch,
-     }).your-entities;
+     }).api.v1['your-entities'];
      ```
 
 6. **前端调用**
@@ -450,73 +450,21 @@
      ```typescript
      import { hc } from 'hono/client';
      import { YourEntityRoutes } from '@/server/api';
-     import { axiosFetch } from '@/client/utils/fetch';
      
      export const yourEntityClient = hc<YourEntityRoutes>('/api/v1', {
        fetch: axiosFetch,
-     }).your_entities; // 注意: 路由路径中的连字符会转为下划线
-     
-     // 类型提取
-     import type { InferRequestType, InferResponseType } from 'hono/client';
-     
-     // 列表查询
-     export type YourEntityListQuery = InferRequestType<typeof yourEntityClient.$get>['query'];
-     export type YourEntityListResponse = InferResponseType<typeof yourEntityClient.$get, 200>;
-     
-     // 创建实体
-     export type CreateYourEntityRequest = InferRequestType<typeof yourEntityClient.$post>['json'];
-     export type CreateYourEntityResponse = InferResponseType<typeof yourEntityClient.$post, 200>;
-     
-     // 获取单个实体
-     export type GetYourEntityParams = InferRequestType<typeof yourEntityClient[':id']['$get']>['param'];
-     export type GetYourEntityResponse = InferResponseType<typeof yourEntityClient[':id']['$get'], 200>;
-     
-     // 更新实体
-     export type UpdateYourEntityParams = InferRequestType<typeof yourEntityClient[':id']['$put']>['param'];
-     export type UpdateYourEntityRequest = InferRequestType<typeof yourEntityClient[':id']['$put']>['json'];
-     export type UpdateYourEntityResponse = InferResponseType<typeof yourEntityClient[':id']['$put'], 200>;
-     
-     // 删除实体
-     export type DeleteYourEntityParams = InferRequestType<typeof yourEntityClient[':id']['$delete']>['param'];
-     export type DeleteYourEntityResponse = InferResponseType<typeof yourEntityClient[':id']['$delete'], 200>;
+     }).api.v1['your-entities'];
      ```
 
-### 7. **前端调用示例**
-   - 在页面组件中使用客户端API:
-     ```typescript
-     import { yourEntityClient, type YourEntityListResponse } from '@/client/api';
-     import { useQuery, useMutation, useQueryClient } from 'react-query';
-     
-     // 获取列表数据
-     const fetchEntities = async () => {
-       const res = await yourEntityClient.$get({
-         query: { page: 1, pageSize: 10, status: 1 }
-       });
-       if (!res.ok) {
-         const error = await res.json();
-         throw new Error(error.message || '获取数据失败');
-       }
-       return res.json() as Promise<YourEntityListResponse>;
-     };
-     
-     // 使用React Query获取数据
-     export function useEntitiesData() {
-       return useQuery(['entities'], fetchEntities);
-     }
-     
-     // 创建实体
-     export function useCreateEntity() {
-       const queryClient = useQueryClient();
-       return useMutation(
-         (data) => yourEntityClient.$post({ json: data }),
-         {
-           onSuccess: () => {
-             queryClient.invalidateQueries(['entities']);
-           }
-         }
-       );
-     }
-     ```
+### 7. **前端调用**
+   - 在页面组件(如`pages_users.tsx`)中:
+     - 使用`InferResponseType`提取响应类型
+     - 使用`InferRequestType`提取请求类型
+     - 示例:
+       ```typescript
+       type EntityResponse = InferResponseType<typeof entityClient.$get, 200>;
+       type CreateRequest = InferRequestType<typeof entityClient.$post>['json'];
+       ```
 
 ## 注意事项
 

+ 45 - 0
.roo/rules/13-ui-style.md

@@ -45,6 +45,51 @@
 - 表单布局使用垂直布局,标签在上,输入框在下
 - 输入框聚焦状态:`focus:border-primary focus:ring-1 focus:ring-primary`
 
+### 3.5 日期表单组件
+- 日期选择器使用 `DatePicker` 组件,时间选择使用 `TimePicker` 组件
+- 日期选择器大小与输入框保持一致:`size="middle"`
+- 日期格式统一为 `YYYY-MM-DD`,时间格式为 `HH:mm:ss`
+- 日期范围选择使用 `RangePicker` 组件,格式为 `[YYYY-MM-DD, YYYY-MM-DD]`
+- 日期选择器添加清除按钮:`allowClear`
+- 日期选择器添加占位提示:`placeholder="请选择日期"`
+- 日期选择器禁用未来日期:`disabledDate={(current) => current && current > dayjs().endOf('day')}`(根据业务需求调整)
+- 日期对象规范:始终使用dayjs对象而非原生Date对象,避免出现"isValid is not a function"错误
+  ```typescript
+  // 错误示例 - 使用原生Date对象
+  form.setFieldsValue({
+    noteDate: new Date(record.noteDate) // 导致验证失败
+  });
+  
+  // 正确示例 - 使用dayjs对象
+  form.setFieldsValue({
+    noteDate: dayjs(record.noteDate) // 正常支持验证方法
+  });
+  ```
+- 日期时间转换规范:
+  ```typescript
+  // 日期对象转字符串(提交给后端)
+  const formatDate = (date: Dayjs | null) => {
+    return date ? date.format('YYYY-MM-DD') : '';
+  };
+  
+  // 字符串转日期对象(从后端接收)
+  const parseDate = (str: string) => {
+    return str ? dayjs(str) : null;
+  };
+  
+  // 日期时间对象转字符串
+  const formatDateTime = (date: Dayjs | null) => {
+    return date ? date.format('YYYY-MM-DD HH:mm:ss') : '';
+  };
+  
+  // 日期范围转换
+  const formatDateRange = (range: [Dayjs | null, Dayjs | null]) => {
+    return range && range[0] && range[1]
+      ? [range[0].format('YYYY-MM-DD'), range[1].format('YYYY-MM-DD')]
+      : [];
+  };
+  ```
+
 ### 3.3 表格样式
 - 表格添加边框:`bordered`
 - 表头背景色使用浅灰(#f9fafb)

+ 143 - 0
README.md

@@ -0,0 +1,143 @@
+# 常见错误排查指南
+
+## 前端常见错误
+
+### 日期组件错误
+
+**错误表现**:  
+`date4.isValid is not a function`  
+`TypeError: date4.isValid is not a function`
+
+**错误原因**:  
+使用原生`Date`对象而非`dayjs`对象初始化日期组件
+
+**错误示例**:
+```typescript
+// 错误示例 - 使用原生Date对象
+form.setFieldsValue({
+  noteDate: new Date(record.noteDate) // 导致验证失败
+});
+```
+
+**正确做法**:
+```typescript
+// 正确示例 - 使用dayjs对象
+form.setFieldsValue({
+  noteDate: dayjs(record.noteDate) // 正常支持验证方法
+});
+```
+
+## 后端常见错误
+
+### OpenAPI路由定义错误
+
+#### 1. 路径参数定义错误
+
+**错误表现**:  
+路径参数无法正确解析,接口调用404
+
+**错误原因**:  
+使用冒号`:`定义路径参数而非花括号`{}`
+
+**错误示例**:
+```typescript
+// 错误方式
+path: '/:id'
+```
+
+**正确做法**:
+```typescript
+// 正确方式
+path: '/{id}'
+```
+
+#### 2. 参数类型转换错误
+
+**错误表现**:  
+数字/布尔型URL参数验证失败,提示类型错误
+
+**错误原因**:  
+未使用`z.coerce`处理URL字符串参数到目标类型的转换
+
+**错误示例**:
+```typescript
+// 错误方式 - 直接使用z.number()
+z.number().int().positive() // 无法处理字符串参数
+
+// 错误方式 - 直接使用z.boolean()
+z.boolean() // 无法处理字符串参数
+```
+
+**正确做法**:
+```typescript
+// 正确方式 - 使用z.coerce.number()
+z.coerce.number().int().positive() // 自动转换字符串参数
+
+// 正确方式 - 使用z.coerce.boolean()
+z.coerce.boolean() // 自动转换字符串参数
+```
+
+### 实体定义错误
+
+#### 1. 创建/更新Schema缺少coerce
+
+**错误表现**:  
+日期/数字类型参数验证失败,接口返回400错误
+
+**错误原因**:  
+实体的创建/更新Schema中,日期、数字等类型未使用`z.coerce`进行类型转换
+
+**正确做法**:
+```typescript
+export const UpdateEntityDto = z.object({
+  price: z.coerce.number().multipleOf(0.01).optional().openapi({
+    description: '价格',
+    example: 89.99
+  }),
+  isActive: z.coerce.boolean().optional().openapi({
+    description: '是否激活',
+    example: false
+  }),
+  expireDate: z.coerce.date().optional().openapi({
+    description: '过期日期',
+    example: '2025-12-31T23:59:59Z'
+  })
+});
+```
+
+## RPC调用错误
+
+### InferResponseType使用错误
+
+**错误表现**:  
+TypeScript类型推断失败,提示属性不存在
+
+**错误原因**:  
+访问带参数的路由类型时未正确使用数组语法
+
+**错误示例**:
+```typescript
+// 错误方式
+InferResponseType<typeof zichanClient[':id'].$get, 200>
+```
+
+**正确做法**:
+```typescript
+// 正确方式 - $get要加中括号
+InferResponseType<typeof zichanClient[':id']['$get'], 200>
+```
+
+## 前后端交互错误
+
+### 表单验证规则不匹配
+
+**错误表现**:  
+前端表单提交后后端验证失败,或前端验证通过但后端返回400
+
+**错误原因**:  
+前端表单的必填/选填规则与后端实体的create/update schema不一致
+
+**正确做法**:
+1. 前端表单验证规则必须与后端保持一致
+2. 后端实体schema变更时需同步更新前端表单验证
+3. 创建操作使用`CreateXXXDto`,更新操作使用`UpdateXXXDto`

+ 8 - 2
src/client/api.ts

@@ -41,9 +41,15 @@ const axiosFetch = async (url: RequestInfo | URL, init?: RequestInit) => {
   }
     
   
+  // 处理204 No Content响应,不设置body
+  const body = response.status === 204
+    ? null
+    : responseHeaders.get('content-type')?.includes('application/json')
+      ? JSON.stringify(response.data)
+      : response.data;
+  
   return new Response(
-    responseHeaders.get('content-type')?.includes('application/json') ? 
-      JSON.stringify(response.data) : response.data, 
+    body,
     {
       status: response.status,
       statusText: response.statusText,