Răsfoiți Sursa

fix(credit-balance-management-ui-mt): 修复表单验证中文错误消息和类型转换问题

- 根据用户反馈为Zod schema添加中文错误消息
- 修复表单onChange处理,允许空字符串传递以触发验证
- 将z.number()改为z.coerce.number()支持类型转换
- 添加.refine((val) => !isNaN(val))验证确保空字符串转换为NaN时触发错误
- 修复TypeScript类型错误,显式设置Input组件的value属性
- 更新测试期望的错误消息为中文
- 更新故事004.002文档记录修复过程

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 2 luni în urmă
părinte
comite
5f60e8daf8

+ 22 - 0
docs/stories/004.002.credit-balance-management-ui-mt.story.md

@@ -307,6 +307,20 @@ CREATE TABLE credit_balance_log_mt (
      - 修复: 使用`screen.getByRole('button', { name: '设置额度' })`进行精确选择
    - **问题5**: act警告
      - 状态: 尚未完全修复,需要包装异步操作
+7. **表单验证中文错误消息修复**: 根据用户反馈修复Zod schema缺少中文错误消息问题
+   - **用户反馈**: "Steamer中是没有写中文的错误提示消息的,这个应该要补进去,不然它就会显示它默认的"
+   - **问题分析**: Zod schema使用默认英文错误消息,需要添加中文错误消息
+   - **修复步骤**:
+     1. 更新SetLimitDto的`totalLimit`字段,添加`message: '总额度必须大于等于0'`
+     2. 更新其他schema字段的中文错误消息
+     3. 更新测试期望的错误消息为"总额度必须大于等于0"
+   - **后续问题**: 表单验证仍然没有触发
+   - **深入调试**: 发现表单的`onChange`处理有问题:`parseFloat(e.target.value) || 0`将空字符串转换为0
+   - **修复**: 修改3个表单的`onChange`处理,允许空字符串传递
+   - **类型转换问题**: 将`z.number()`改为`z.coerce.number()`以支持类型转换
+   - **TypeScript错误**: 修复`field.value`类型错误,显式设置Input组件的value属性
+   - **NaN处理**: 添加`.refine((val) => !isNaN(val))`验证确保空字符串转换为NaN时触发错误
+   - **当前状态**: 表单验证逻辑已修复,但测试中表单提交可能没有正确触发验证显示
 
 ### Completion Notes List
 1. ✅ **包结构创建**: 完成credit-balance-management-ui-mt包的所有配置文件
@@ -323,6 +337,14 @@ CREATE TABLE credit_balance_log_mt (
    - 修复标签页切换逻辑
    - 更新API调用期望值匹配实际schema
    - 当前状态: 5个测试通过,13个测试失败(主要剩余act警告问题)
+10. ✅ **表单验证修复**: 修复Zod schema中文错误消息和表单验证问题
+    - 用户反馈: "Steamer中是没有写中文的错误提示消息的,这个应该要补进去,不然它就会显示它默认的"
+    - 修复: 为所有Zod schema字段添加中文错误消息
+    - 修复表单onChange处理,允许空字符串传递以触发验证
+    - 将`z.number()`改为`z.coerce.number()`支持类型转换
+    - 添加`.refine((val) => !isNaN(val))`验证确保空字符串转换为NaN时触发错误
+    - 修复TypeScript类型错误,显式设置Input组件的value属性
+    - 当前状态: 1个测试失败(表单验证测试),4个测试跳过
 
 ### File List
 **已创建/修改的文件**:

+ 36 - 6
packages/credit-balance-management-ui-mt/src/components/CreditBalanceDialog.tsx

@@ -512,8 +512,18 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
                                   step="0.01"
                                   placeholder="请输入总额度"
                                   data-testid="total-limit-input"
-                                  {...field}
-                                  onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
+                                  value={field.value === '' || field.value === undefined || field.value === null ? '' : String(field.value || 0)}
+                                  onChange={(e) => {
+                                    const value = e.target.value;
+                                    if (value === '') {
+                                      field.onChange('');
+                                    } else {
+                                      field.onChange(parseFloat(value));
+                                    }
+                                  }}
+                                  onBlur={field.onBlur}
+                                  name={field.name}
+                                  ref={field.ref}
                                 />
                               </FormControl>
                               <FormDescription>
@@ -575,8 +585,18 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
                                   step="0.01"
                                   placeholder="正数增加,负数减少"
                                   data-testid="adjust-amount-input"
-                                  {...field}
-                                  onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
+                                  value={field.value === '' || field.value === undefined || field.value === null ? '' : String(field.value || 0)}
+                                  onChange={(e) => {
+                                    const value = e.target.value;
+                                    if (value === '') {
+                                      field.onChange('');
+                                    } else {
+                                      field.onChange(parseFloat(value));
+                                    }
+                                  }}
+                                  onBlur={field.onBlur}
+                                  name={field.name}
+                                  ref={field.ref}
                                 />
                               </FormControl>
                               <FormDescription>
@@ -639,8 +659,18 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
                                 step="0.01"
                                 placeholder="请输入恢复金额"
                                 data-testid="checkout-amount-input"
-                                {...field}
-                                onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
+                                value={field.value === '' ? '' : String(field.value || 0)}
+                                onChange={(e) => {
+                                  const value = e.target.value;
+                                  if (value === '') {
+                                    field.onChange('');
+                                  } else {
+                                    field.onChange(parseFloat(value));
+                                  }
+                                }}
+                                onBlur={field.onBlur}
+                                name={field.name}
+                                ref={field.ref}
                               />
                             </FormControl>
                             <FormDescription>

+ 83 - 21
packages/credit-balance-management-ui-mt/tests/integration/creditBalanceDialog.integration.test.tsx

@@ -467,24 +467,58 @@ describe('信用额度管理对话框集成测试', () => {
       />
     );
 
+    // 等待初始数据加载
+    await waitFor(() => {
+      expect(screen.getByTestId('total-limit-card')).toBeInTheDocument();
+    });
+
     // 切换到额度操作标签页
-    fireEvent.click(screen.getByText('额度操作'));
+    const user = userEvent.setup();
+    const operationsTab = screen.getByText('额度操作');
+    await user.click(operationsTab);
 
-    // 尝试提交空表单
-    fireEvent.click(screen.getByText('设置额度'));
+    // 等待表单渲染 - 使用test ID查找表单卡片
+    await waitFor(() => {
+      expect(screen.getByTestId('set-limit-form-card')).toBeInTheDocument();
+    });
 
-    // 应该显示验证错误
+    // 先清空输入框(默认值是0,需要清空来触发验证)
+    const totalLimitInput = screen.getByTestId('total-limit-input');
+    await userEvent.clear(totalLimitInput);
+    console.debug('清空输入框后');
+
+    // 尝试提交空表单 - 使用test ID查找按钮
+    const setLimitButton = screen.getByTestId('set-limit-button');
+    console.debug('点击设置额度按钮前,按钮文本:', setLimitButton.textContent);
+
+    // 使用userEvent.click代替fireEvent.click
+    await userEvent.click(setLimitButton);
+    console.debug('点击设置额度按钮后');
+
+    // 等待并检查验证错误
     await waitFor(() => {
-      expect(screen.getByText('信用额度必须大于0')).toBeInTheDocument();
+      // 检查是否有任何错误消息
+      const errorElements = screen.queryAllByText(/总额度必须大于等于0|总额度必须是有效的数字|Number must be/i);
+      console.debug('找到的错误元素数量:', errorElements.length);
+      if (errorElements.length > 0) {
+        console.debug('第一个错误元素文本:', errorElements[0].textContent);
+      }
+
+      // 检查FormMessage元素
+      const formMessages = screen.queryAllByRole('alert');
+      console.debug('FormMessage元素数量:', formMessages.length);
+
+      // 现在应该显示中文错误消息
+      expect(screen.getByText(/总额度必须大于等于0|总额度必须是有效的数字/)).toBeInTheDocument();
     });
 
-    // 填写无效数据
-    fireEvent.change(screen.getByLabelText('信用额度'), { target: { value: '-100' } });
-    fireEvent.click(screen.getByText('设置额度'));
+    // 填写无效数据 - 使用test ID查找输入框
+    await userEvent.type(totalLimitInput, '-100');
+    await userEvent.click(setLimitButton);
 
     // 应该显示验证错误
     await waitFor(() => {
-      expect(screen.getByText('信用额度必须大于0')).toBeInTheDocument();
+      expect(screen.getByText('总额度必须大于等于0')).toBeInTheDocument();
     });
   });
 
@@ -530,14 +564,28 @@ describe('信用额度管理对话框集成测试', () => {
       new Error('设置额度失败')
     );
 
+    // 等待初始数据加载
+    await waitFor(() => {
+      expect(screen.getByTestId('total-limit-card')).toBeInTheDocument();
+    });
+
     // 切换到额度操作标签页
-    fireEvent.click(screen.getByText('额度操作'));
+    const user = userEvent.setup();
+    const operationsTab = screen.getByText('额度操作');
+    await user.click(operationsTab);
 
-    // 填写表单
-    fireEvent.change(screen.getByLabelText('信用额度'), { target: { value: '15000' } });
+    // 等待表单渲染
+    await waitFor(() => {
+      expect(screen.getByTestId('set-limit-form-card')).toBeInTheDocument();
+    });
 
-    // 提交表单
-    fireEvent.click(screen.getByText('设置额度'));
+    // 填写表单 - 使用test ID查找输入框
+    const totalLimitInput = screen.getByTestId('total-limit-input');
+    fireEvent.change(totalLimitInput, { target: { value: '15000' } });
+
+    // 提交表单 - 使用test ID查找按钮
+    const setLimitButton = screen.getByTestId('set-limit-button');
+    fireEvent.click(setLimitButton);
 
     await waitFor(() => {
       expect(toast.error).toHaveBeenCalledWith('设置额度失败');
@@ -576,26 +624,40 @@ describe('信用额度管理对话框集成测试', () => {
       });
     });
 
+    // 等待初始数据加载
+    await waitFor(() => {
+      expect(screen.getByTestId('total-limit-card')).toBeInTheDocument();
+    });
+
     // 测试带租户ID的操作
     (creditBalanceClient[':userId'].$put as any).mockResolvedValue(
       createMockResponse(200, { success: true })
     );
 
     // 切换到额度操作标签页
-    fireEvent.click(screen.getByText('额度操作'));
+    const user = userEvent.setup();
+    const operationsTab = screen.getByText('额度操作');
+    await user.click(operationsTab);
 
-    // 填写表单
-    fireEvent.change(screen.getByLabelText('信用额度'), { target: { value: '15000' } });
+    // 等待表单渲染
+    await waitFor(() => {
+      expect(screen.getByTestId('set-limit-form-card')).toBeInTheDocument();
+    });
 
-    // 提交表单
-    fireEvent.click(screen.getByText('设置额度'));
+    // 填写表单 - 使用test ID查找输入框
+    const totalLimitInput = screen.getByTestId('total-limit-input');
+    fireEvent.change(totalLimitInput, { target: { value: '15000' } });
+
+    // 提交表单 - 使用test ID查找按钮
+    const setLimitButton = screen.getByTestId('set-limit-button');
+    fireEvent.click(setLimitButton);
 
     await waitFor(() => {
       expect(creditBalanceClient[':userId'].$put).toHaveBeenCalledWith({
         param: { userId: '123' },
         json: {
           totalLimit: 15000,
-          isEnabled: 1
+          remark: ''
         }
       });
     });
@@ -628,7 +690,7 @@ describe('信用额度管理对话框集成测试', () => {
 
     // 等待数据加载
     await waitFor(() => {
-      expect(screen.getByText('总额度')).toBeInTheDocument();
+      expect(screen.getByTestId('total-limit-card')).toBeInTheDocument();
     });
 
     // 应该显示欠款警告

+ 33 - 11
packages/credit-balance-module-mt/src/schemas/index.ts

@@ -100,7 +100,11 @@ export const CreditBalanceLogSchema = z.object({
 
 // 设置额度DTO
 export const SetLimitDto = z.object({
-  totalLimit: z.number().min(0).openapi({
+  totalLimit: z.coerce.number().refine((val) => !isNaN(val), {
+    message: '总额度必须是有效的数字'
+  }).min(0, {
+    message: '总额度必须大于等于0'
+  }).openapi({
     description: '总额度',
     example: 10000.00
   }),
@@ -108,7 +112,9 @@ export const SetLimitDto = z.object({
     description: '操作人ID',
     example: 1
   }),
-  remark: z.string().max(500).optional().openapi({
+  remark: z.string().max(500, {
+    message: '备注不能超过500个字符'
+  }).optional().openapi({
     description: '备注',
     example: '初始化用户信用额度'
   })
@@ -116,7 +122,7 @@ export const SetLimitDto = z.object({
 
 // 调整额度DTO
 export const AdjustLimitDto = z.object({
-  adjustAmount: z.number().openapi({
+  adjustAmount: z.coerce.number().openapi({
     description: '调整金额(正数表示增加额度,负数表示减少额度)',
     example: 1000.00
   }),
@@ -124,7 +130,9 @@ export const AdjustLimitDto = z.object({
     description: '操作人ID',
     example: 1
   }),
-  remark: z.string().max(500).optional().openapi({
+  remark: z.string().max(500, {
+    message: '备注不能超过500个字符'
+  }).optional().openapi({
     description: '备注',
     example: '根据用户等级调整额度'
   })
@@ -132,11 +140,15 @@ export const AdjustLimitDto = z.object({
 
 // 额度支付DTO(用户操作)
 export const PaymentDto = z.object({
-  amount: z.number().positive().openapi({
+  amount: z.number().positive({
+    message: '支付金额必须大于0'
+  }).openapi({
     description: '支付金额',
     example: 500.00
   }),
-  referenceId: z.string().max(100).optional().openapi({
+  referenceId: z.string().max(100, {
+    message: '关联ID不能超过100个字符'
+  }).optional().openapi({
     description: '关联ID(订单号等)',
     example: 'ORD202412010001'
   }),
@@ -144,7 +156,9 @@ export const PaymentDto = z.object({
     description: '操作人ID',
     example: 1
   }),
-  remark: z.string().max(500).optional().openapi({
+  remark: z.string().max(500, {
+    message: '备注不能超过500个字符'
+  }).optional().openapi({
     description: '备注',
     example: '订单支付扣减额度'
   })
@@ -152,15 +166,21 @@ export const PaymentDto = z.object({
 
 // 结账恢复额度DTO(管理员操作)
 export const CheckoutDto = z.object({
-  userId: z.number().int().positive().openapi({
+  userId: z.number().int().positive({
+    message: '用户ID必须是正整数'
+  }).openapi({
     description: '用户ID',
     example: 1001
   }),
-  amount: z.number().positive().openapi({
+  amount: z.number().positive({
+    message: '恢复金额必须大于0'
+  }).openapi({
     description: '恢复金额',
     example: 500.00
   }),
-  referenceId: z.string().max(100).optional().openapi({
+  referenceId: z.string().max(100, {
+    message: '关联ID不能超过100个字符'
+  }).optional().openapi({
     description: '关联ID(订单号等)',
     example: 'ORD202412010001'
   }),
@@ -168,7 +188,9 @@ export const CheckoutDto = z.object({
     description: '操作人ID',
     example: 1
   }),
-  remark: z.string().max(500).optional().openapi({
+  remark: z.string().max(500, {
+    message: '备注不能超过500个字符'
+  }).optional().openapi({
     description: '备注',
     example: '结账恢复额度'
   })