Prechádzať zdrojové kódy

✨ feat(BatchSpecCreatorInline): 重构批量规格创建表单,集成 react-hook-form 与 zod 验证

- 引入 react-hook-form 和 zod 库,实现表单状态管理和数据验证
- 定义 addSpecFormSchema 验证规则,确保规格名称、价格、成本价和库存的输入有效性
- 将原有的手动状态管理替换为 Form 组件,简化表单结构并提升可维护性
- 添加表单提交处理函数 onSubmit 和错误处理函数 onError,优化用户体验
- 保留所有原有的数据测试标识符,确保测试兼容性
yourname 1 mesiac pred
rodič
commit
753fddf754

+ 167 - 99
packages/goods-management-ui-mt/src/components/BatchSpecCreatorInline.tsx

@@ -1,6 +1,9 @@
 import React, { useState } from 'react';
 import { Plus, Trash2, Copy, Save, X, Package } from 'lucide-react';
 import { toast } from 'sonner';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import * as z from 'zod';
 
 import { Button } from '@d8d/shared-ui-components/components/ui/button';
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
@@ -8,6 +11,7 @@ import { Input } from '@d8d/shared-ui-components/components/ui/input';
 import { Label } from '@d8d/shared-ui-components/components/ui/label';
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table';
 import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
 
 interface BatchSpecCreatorInlineProps {
   // 初始规格模板
@@ -28,6 +32,21 @@ interface BatchSpecCreatorInlineProps {
   disabled?: boolean;
 }
 
+// 添加规格表单的schema
+const addSpecFormSchema = z.object({
+  name: z.string().min(1, '规格名称不能为空').trim(),
+  price: z.number().min(0, '价格不能为负数'),
+  costPrice: z.number().min(0, '成本价不能为负数'),
+  stock: z.number().int().min(0, '库存不能为负数'),
+});
+
+type AddSpecFormValues = {
+  name: string;
+  price: number;
+  costPrice: number;
+  stock: number;
+};
+
 export const BatchSpecCreatorInline: React.FC<BatchSpecCreatorInlineProps> = ({
   initialSpecs = [],
   onSpecsChange,
@@ -49,50 +68,34 @@ export const BatchSpecCreatorInline: React.FC<BatchSpecCreatorInlineProps> = ({
     }))
   );
 
-  const [newSpec, setNewSpec] = useState({
-    name: '',
-    price: 0,
-    costPrice: 0,
-    stock: 0,
-    sort: 0
-  });
-
   const [templateName, setTemplateName] = useState('');
   const [showSaveTemplate, setShowSaveTemplate] = useState(false);
 
-  const handleAddSpec = () => {
-    if (!newSpec.name.trim()) {
-      toast.error('请输入规格名称');
-      return;
-    }
-
-    if (newSpec.price < 0) {
-      toast.error('价格不能为负数');
-      return;
-    }
-
-    if (newSpec.costPrice < 0) {
-      toast.error('成本价不能为负数');
-      return;
-    }
-
-    if (newSpec.stock < 0) {
-      toast.error('库存不能为负数');
-      return;
-    }
+  // 添加规格表单
+  const form = useForm<AddSpecFormValues>({
+    resolver: zodResolver(addSpecFormSchema),
+    defaultValues: {
+      name: '',
+      price: 0,
+      costPrice: 0,
+      stock: 0,
+    },
+  });
 
+  const onSubmit = (data: AddSpecFormValues) => {
     // 检查规格名称是否重复(不区分大小写)
     const isDuplicate = specs.some(spec =>
-      spec.name.toLowerCase() === newSpec.name.trim().toLowerCase()
+      spec.name.toLowerCase() === data.name.trim().toLowerCase()
     );
     if (isDuplicate) {
-      toast.error(`规格名称 "${newSpec.name}" 已存在,请使用不同的名称`);
+      toast.error(`规格名称 "${data.name}" 已存在,请使用不同的名称`);
       return;
     }
 
     const newSpecWithId = {
       id: Date.now(),
-      ...newSpec
+      ...data,
+      sort: specs.length
     };
 
     const updatedSpecs = [...specs, newSpecWithId];
@@ -104,17 +107,25 @@ export const BatchSpecCreatorInline: React.FC<BatchSpecCreatorInlineProps> = ({
     }
 
     // 重置表单
-    setNewSpec({
+    form.reset({
       name: '',
       price: 0,
       costPrice: 0,
       stock: 0,
-      sort: specs.length
     });
 
     toast.success('规格已添加');
   };
 
+  const onError = (errors: any) => {
+    // 显示第一个错误消息
+    const firstError = Object.values(errors)[0] as any;
+    if (firstError?.message) {
+      toast.error(firstError.message);
+    }
+  };
+
+
   const handleRemoveSpec = (id: number) => {
     const updatedSpecs = specs.filter(spec => spec.id !== id);
     setSpecs(updatedSpecs);
@@ -262,72 +273,129 @@ export const BatchSpecCreatorInline: React.FC<BatchSpecCreatorInlineProps> = ({
       </CardHeader>
       <CardContent className="space-y-4">
         {/* 添加新规格表单 */}
-        <div className="grid grid-cols-1 md:grid-cols-6 gap-4 p-4 border rounded-lg" data-testid="add-spec-form">
-          <div className="md:col-span-2">
-            <Label htmlFor="spec-name" data-testid="spec-name-label">规格名称 *</Label>
-            <Input
-              id="spec-name"
-              data-testid="spec-name-input"
-              placeholder="例如:红色、64GB、S码"
-              value={newSpec.name}
-              onChange={(e) => setNewSpec({ ...newSpec, name: e.target.value })}
-              disabled={disabled}
-            />
-          </div>
-          <div>
-            <Label htmlFor="spec-price" data-testid="spec-price-label">价格</Label>
-            <Input
-              id="spec-price"
-              data-testid="spec-price-input"
-              type="number"
-              min="0"
-              step="0.01"
-              placeholder="0.00"
-              value={newSpec.price}
-              onChange={(e) => setNewSpec({ ...newSpec, price: parseFloat(e.target.value) || 0 })}
-              disabled={disabled}
-            />
-          </div>
-          <div>
-            <Label htmlFor="spec-cost-price" data-testid="spec-cost-price-label">成本价</Label>
-            <Input
-              id="spec-cost-price"
-              data-testid="spec-cost-price-input"
-              type="number"
-              min="0"
-              step="0.01"
-              placeholder="0.00"
-              value={newSpec.costPrice}
-              onChange={(e) => setNewSpec({ ...newSpec, costPrice: parseFloat(e.target.value) || 0 })}
-              disabled={disabled}
-            />
-          </div>
-          <div>
-            <Label htmlFor="spec-stock" data-testid="spec-stock-label">库存</Label>
-            <Input
-              id="spec-stock"
-              data-testid="spec-stock-input"
-              type="number"
-              min="0"
-              step="1"
-              placeholder="0"
-              value={newSpec.stock}
-              onChange={(e) => setNewSpec({ ...newSpec, stock: parseInt(e.target.value) || 0 })}
-              disabled={disabled}
-            />
-          </div>
-          <div className="flex items-end">
-            <Button
-              onClick={handleAddSpec}
-              disabled={disabled || !newSpec.name.trim()}
-              className="w-full"
-              data-testid="add-spec-button"
-            >
-              <Plus className="mr-2 h-4 w-4" />
-              添加
-            </Button>
-          </div>
-        </div>
+        <Form {...form}>
+          <form
+            onSubmit={form.handleSubmit(onSubmit, onError)}
+            className="grid grid-cols-1 md:grid-cols-6 gap-4 p-4 border rounded-lg"
+            data-testid="add-spec-form"
+          >
+            <div className="md:col-span-2">
+              <FormField
+                control={form.control}
+                name="name"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel data-testid="spec-name-label">规格名称 *</FormLabel>
+                    <FormControl>
+                      <Input
+                        placeholder="例如:红色、64GB、S码"
+                        data-testid="spec-name-input"
+                        disabled={disabled}
+                        {...field}
+                        onChange={(e) => {
+                          field.onChange(e.target.value);
+                        }}
+                      />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+            </div>
+            <div>
+              <FormField
+                control={form.control}
+                name="price"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel data-testid="spec-price-label">价格</FormLabel>
+                    <FormControl>
+                      <Input
+                        type="number"
+                        min="0"
+                        step="0.01"
+                        placeholder="0.00"
+                        data-testid="spec-price-input"
+                        disabled={disabled}
+                        {...field}
+                        onChange={(e) => {
+                          field.onChange(parseFloat(e.target.value) || 0);
+                        }}
+                        value={field.value === 0 ? '' : field.value}
+                      />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+            </div>
+            <div>
+              <FormField
+                control={form.control}
+                name="costPrice"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel data-testid="spec-cost-price-label">成本价</FormLabel>
+                    <FormControl>
+                      <Input
+                        type="number"
+                        min="0"
+                        step="0.01"
+                        placeholder="0.00"
+                        data-testid="spec-cost-price-input"
+                        disabled={disabled}
+                        {...field}
+                        onChange={(e) => {
+                          field.onChange(parseFloat(e.target.value) || 0);
+                        }}
+                        value={field.value === 0 ? '' : field.value}
+                      />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+            </div>
+            <div>
+              <FormField
+                control={form.control}
+                name="stock"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel data-testid="spec-stock-label">库存</FormLabel>
+                    <FormControl>
+                      <Input
+                        type="number"
+                        min="0"
+                        step="1"
+                        placeholder="0"
+                        data-testid="spec-stock-input"
+                        disabled={disabled}
+                        {...field}
+                        onChange={(e) => {
+                          field.onChange(parseInt(e.target.value) || 0);
+                        }}
+                        value={field.value === 0 ? '' : field.value}
+                      />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+            </div>
+            <div className="flex items-end">
+              <Button
+                type="submit"
+                disabled={disabled || !form.watch('name').trim()}
+                className="w-full"
+                data-testid="add-spec-button"
+              >
+                <Plus className="mr-2 h-4 w-4" />
+                添加
+              </Button>
+            </div>
+          </form>
+        </Form>
 
         {/* 预定义模板 */}
         <div data-testid="predefined-templates-section">