|
|
@@ -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">
|