|
|
@@ -1,10 +1,15 @@
|
|
|
-import React, { useState } from 'react';
|
|
|
+import React from 'react';
|
|
|
+import { useForm } from 'react-hook-form';
|
|
|
+import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
import { Save, X } from 'lucide-react';
|
|
|
|
|
|
import { Button } from '@d8d/shared-ui-components/components/ui/button';
|
|
|
import { Input } from '@d8d/shared-ui-components/components/ui/input';
|
|
|
-import { Label } from '@d8d/shared-ui-components/components/ui/label';
|
|
|
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
|
|
|
+import { AdminUpdateGoodsDto } from '@d8d/goods-module-mt/schemas';
|
|
|
+import { UpdateRequest } from '../types/goods';
|
|
|
+import { z } from 'zod';
|
|
|
|
|
|
interface ChildGoodsInlineEditFormProps {
|
|
|
// 子商品数据
|
|
|
@@ -19,251 +24,227 @@ interface ChildGoodsInlineEditFormProps {
|
|
|
};
|
|
|
|
|
|
// 回调函数
|
|
|
- onSave: (childId: number, updateData: {
|
|
|
- name: string;
|
|
|
- price: number;
|
|
|
- costPrice?: number;
|
|
|
- stock: number;
|
|
|
- sort: number;
|
|
|
- state: number;
|
|
|
- }) => Promise<void>;
|
|
|
+ onSave: (childId: number, updateData: UpdateRequest) => Promise<void>;
|
|
|
onCancel: () => void;
|
|
|
|
|
|
// 加载状态
|
|
|
isLoading?: boolean;
|
|
|
}
|
|
|
|
|
|
+// 从AdminUpdateGoodsDto提取我们需要的字段,并设置为必需
|
|
|
+const childGoodsUpdateSchema = AdminUpdateGoodsDto.pick({
|
|
|
+ name: true,
|
|
|
+ price: true,
|
|
|
+ costPrice: true,
|
|
|
+ stock: true,
|
|
|
+ sort: true,
|
|
|
+ state: true
|
|
|
+}).extend({
|
|
|
+ name: z.string().min(1, '商品名称不能为空'),
|
|
|
+ price: z.coerce.number<number>().multipleOf(0.01, '价格最多保留两位小数').min(0, '价格不能为负数'),
|
|
|
+ costPrice: z.coerce.number<number>().multipleOf(0.01, '成本价最多保留两位小数').min(0, '成本价不能为负数').optional(),
|
|
|
+ stock: z.coerce.number<number>().int().nonnegative('库存必须为非负数'),
|
|
|
+ sort: z.coerce.number<number>().int().nonnegative('排序值必须为非负数'),
|
|
|
+ state: z.number().int().min(1).max(2)
|
|
|
+});
|
|
|
+
|
|
|
+// 表单数据类型
|
|
|
+type ChildGoodsFormData = z.infer<typeof childGoodsUpdateSchema>;
|
|
|
+
|
|
|
export const ChildGoodsInlineEditForm: React.FC<ChildGoodsInlineEditFormProps> = ({
|
|
|
child,
|
|
|
onSave,
|
|
|
onCancel,
|
|
|
isLoading = false
|
|
|
}) => {
|
|
|
- // 表单状态
|
|
|
- const [formData, setFormData] = useState({
|
|
|
- name: child.name,
|
|
|
- price: child.price.toString(),
|
|
|
- costPrice: child.costPrice?.toString() || '',
|
|
|
- stock: child.stock.toString(),
|
|
|
- sort: child.sort.toString(),
|
|
|
- state: child.state.toString()
|
|
|
- });
|
|
|
-
|
|
|
- // 表单验证状态
|
|
|
- const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
-
|
|
|
- // 处理输入变化
|
|
|
- const handleInputChange = (field: string, value: string) => {
|
|
|
- setFormData(prev => ({
|
|
|
- ...prev,
|
|
|
- [field]: value
|
|
|
- }));
|
|
|
-
|
|
|
- // 清除该字段的错误
|
|
|
- if (errors[field]) {
|
|
|
- setErrors(prev => {
|
|
|
- const newErrors = { ...prev };
|
|
|
- delete newErrors[field];
|
|
|
- return newErrors;
|
|
|
- });
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 验证表单
|
|
|
- const validateForm = (): boolean => {
|
|
|
- const newErrors: Record<string, string> = {};
|
|
|
-
|
|
|
- // 验证商品名称
|
|
|
- if (!formData.name.trim()) {
|
|
|
- newErrors.name = '商品名称不能为空';
|
|
|
- } else if (formData.name.length > 255) {
|
|
|
- newErrors.name = '商品名称不能超过255个字符';
|
|
|
- }
|
|
|
-
|
|
|
- // 验证价格
|
|
|
- const price = parseFloat(formData.price);
|
|
|
- if (isNaN(price) || price < 0) {
|
|
|
- newErrors.price = '价格必须是非负数';
|
|
|
- }
|
|
|
-
|
|
|
- // 验证成本价(可选)
|
|
|
- if (formData.costPrice) {
|
|
|
- const costPrice = parseFloat(formData.costPrice);
|
|
|
- if (isNaN(costPrice) || costPrice < 0) {
|
|
|
- newErrors.costPrice = '成本价必须是非负数';
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 验证库存
|
|
|
- const stock = parseInt(formData.stock);
|
|
|
- if (isNaN(stock) || stock < 0) {
|
|
|
- newErrors.stock = '库存必须是非负整数';
|
|
|
- }
|
|
|
-
|
|
|
- // 验证排序
|
|
|
- const sort = parseInt(formData.sort);
|
|
|
- if (isNaN(sort) || sort < 0) {
|
|
|
- newErrors.sort = '排序值必须是非负整数';
|
|
|
- }
|
|
|
-
|
|
|
- // 验证状态
|
|
|
- const state = parseInt(formData.state);
|
|
|
- if (isNaN(state) || (state !== 1 && state !== 2)) {
|
|
|
- newErrors.state = '状态值必须是1(可用)或2(不可用)';
|
|
|
+ // 初始化表单
|
|
|
+ const form = useForm<ChildGoodsFormData>({
|
|
|
+ resolver: zodResolver(childGoodsUpdateSchema),
|
|
|
+ defaultValues: {
|
|
|
+ name: child.name,
|
|
|
+ price: child.price,
|
|
|
+ costPrice: child.costPrice,
|
|
|
+ stock: child.stock,
|
|
|
+ sort: child.sort,
|
|
|
+ state: child.state
|
|
|
}
|
|
|
-
|
|
|
- setErrors(newErrors);
|
|
|
- return Object.keys(newErrors).length === 0;
|
|
|
- };
|
|
|
+ });
|
|
|
|
|
|
// 处理表单提交
|
|
|
- const handleSubmit = async (e: React.FormEvent) => {
|
|
|
- e.preventDefault();
|
|
|
-
|
|
|
- if (!validateForm()) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 准备更新数据
|
|
|
+ const handleSubmit = form.handleSubmit(async (data) => {
|
|
|
+ // 准备更新数据 - 直接使用验证后的数据
|
|
|
const updateData = {
|
|
|
- name: formData.name.trim(),
|
|
|
- price: parseFloat(formData.price),
|
|
|
- costPrice: formData.costPrice ? parseFloat(formData.costPrice) : undefined,
|
|
|
- stock: parseInt(formData.stock),
|
|
|
- sort: parseInt(formData.sort),
|
|
|
- state: parseInt(formData.state)
|
|
|
+ name: data.name,
|
|
|
+ price: data.price,
|
|
|
+ costPrice: data.costPrice,
|
|
|
+ stock: data.stock,
|
|
|
+ sort: data.sort,
|
|
|
+ state: data.state
|
|
|
};
|
|
|
|
|
|
await onSave(child.id, updateData);
|
|
|
- };
|
|
|
+ }, (errors) => {
|
|
|
+ console.debug('表单验证错误:', errors);
|
|
|
+ });
|
|
|
|
|
|
return (
|
|
|
- <form onSubmit={handleSubmit} className="space-y-4">
|
|
|
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
- {/* 商品名称 */}
|
|
|
- <div className="space-y-2">
|
|
|
- <Label htmlFor="name">商品名称</Label>
|
|
|
- <Input
|
|
|
- id="name"
|
|
|
- value={formData.name}
|
|
|
- onChange={(e) => handleInputChange('name', e.target.value)}
|
|
|
- placeholder="请输入商品名称"
|
|
|
- className={errors.name ? 'border-destructive' : ''}
|
|
|
+ <Form {...form}>
|
|
|
+ <form onSubmit={handleSubmit} className="space-y-4">
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
+ {/* 商品名称 */}
|
|
|
+ <FormField
|
|
|
+ control={form.control}
|
|
|
+ name="name"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>商品名称</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ placeholder="请输入商品名称"
|
|
|
+ {...field}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
/>
|
|
|
- {errors.name && (
|
|
|
- <p className="text-sm text-destructive">{errors.name}</p>
|
|
|
- )}
|
|
|
- </div>
|
|
|
|
|
|
- {/* 价格 */}
|
|
|
- <div className="space-y-2">
|
|
|
- <Label htmlFor="price">价格</Label>
|
|
|
- <Input
|
|
|
- id="price"
|
|
|
- type="number"
|
|
|
- step="0.01"
|
|
|
- min="0"
|
|
|
- value={formData.price}
|
|
|
- onChange={(e) => handleInputChange('price', e.target.value)}
|
|
|
- placeholder="0.00"
|
|
|
- className={errors.price ? 'border-destructive' : ''}
|
|
|
+ {/* 价格 */}
|
|
|
+ <FormField
|
|
|
+ control={form.control}
|
|
|
+ name="price"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>价格</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ type="number"
|
|
|
+ step="0.01"
|
|
|
+ min="0"
|
|
|
+ placeholder="0.00"
|
|
|
+ {...field}
|
|
|
+ value={field.value ?? ''}
|
|
|
+ onChange={(e) => field.onChange(e.target.value === '' ? undefined : parseFloat(e.target.value))}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
/>
|
|
|
- {errors.price && (
|
|
|
- <p className="text-sm text-destructive">{errors.price}</p>
|
|
|
- )}
|
|
|
- </div>
|
|
|
|
|
|
- {/* 成本价 */}
|
|
|
- <div className="space-y-2">
|
|
|
- <Label htmlFor="costPrice">成本价</Label>
|
|
|
- <Input
|
|
|
- id="costPrice"
|
|
|
- type="number"
|
|
|
- step="0.01"
|
|
|
- min="0"
|
|
|
- value={formData.costPrice}
|
|
|
- onChange={(e) => handleInputChange('costPrice', e.target.value)}
|
|
|
- placeholder="0.00"
|
|
|
- className={errors.costPrice ? 'border-destructive' : ''}
|
|
|
+ {/* 成本价 */}
|
|
|
+ <FormField
|
|
|
+ control={form.control}
|
|
|
+ name="costPrice"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>成本价</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ type="number"
|
|
|
+ step="0.01"
|
|
|
+ min="0"
|
|
|
+ placeholder="0.00"
|
|
|
+ {...field}
|
|
|
+ value={field.value ?? ''}
|
|
|
+ onChange={(e) => field.onChange(e.target.value === '' ? undefined : parseFloat(e.target.value))}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
/>
|
|
|
- {errors.costPrice && (
|
|
|
- <p className="text-sm text-destructive">{errors.costPrice}</p>
|
|
|
- )}
|
|
|
- </div>
|
|
|
|
|
|
- {/* 库存 */}
|
|
|
- <div className="space-y-2">
|
|
|
- <Label htmlFor="stock">库存</Label>
|
|
|
- <Input
|
|
|
- id="stock"
|
|
|
- type="number"
|
|
|
- min="0"
|
|
|
- step="1"
|
|
|
- value={formData.stock}
|
|
|
- onChange={(e) => handleInputChange('stock', e.target.value)}
|
|
|
- placeholder="0"
|
|
|
- className={errors.stock ? 'border-destructive' : ''}
|
|
|
+ {/* 库存 */}
|
|
|
+ <FormField
|
|
|
+ control={form.control}
|
|
|
+ name="stock"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>库存</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ type="number"
|
|
|
+ min="0"
|
|
|
+ step="1"
|
|
|
+ placeholder="0"
|
|
|
+ {...field}
|
|
|
+ value={field.value ?? ''}
|
|
|
+ onChange={(e) => field.onChange(e.target.value === '' ? undefined : parseInt(e.target.value))}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
/>
|
|
|
- {errors.stock && (
|
|
|
- <p className="text-sm text-destructive">{errors.stock}</p>
|
|
|
- )}
|
|
|
- </div>
|
|
|
|
|
|
- {/* 排序 */}
|
|
|
- <div className="space-y-2">
|
|
|
- <Label htmlFor="sort">排序</Label>
|
|
|
- <Input
|
|
|
- id="sort"
|
|
|
- type="number"
|
|
|
- min="0"
|
|
|
- step="1"
|
|
|
- value={formData.sort}
|
|
|
- onChange={(e) => handleInputChange('sort', e.target.value)}
|
|
|
- placeholder="0"
|
|
|
- className={errors.sort ? 'border-destructive' : ''}
|
|
|
+ {/* 排序 */}
|
|
|
+ <FormField
|
|
|
+ control={form.control}
|
|
|
+ name="sort"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>排序</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ type="number"
|
|
|
+ min="0"
|
|
|
+ step="1"
|
|
|
+ placeholder="0"
|
|
|
+ {...field}
|
|
|
+ value={field.value ?? ''}
|
|
|
+ onChange={(e) => field.onChange(e.target.value === '' ? undefined : parseInt(e.target.value))}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ {/* 状态 */}
|
|
|
+ <FormField
|
|
|
+ control={form.control}
|
|
|
+ name="state"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>状态</FormLabel>
|
|
|
+ <Select
|
|
|
+ value={field.value?.toString()}
|
|
|
+ onValueChange={(value) => field.onChange(parseInt(value))}
|
|
|
+ >
|
|
|
+ <FormControl>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="选择状态" />
|
|
|
+ </SelectTrigger>
|
|
|
+ </FormControl>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="1">可用</SelectItem>
|
|
|
+ <SelectItem value="2">不可用</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
/>
|
|
|
- {errors.sort && (
|
|
|
- <p className="text-sm text-destructive">{errors.sort}</p>
|
|
|
- )}
|
|
|
</div>
|
|
|
|
|
|
- {/* 状态 */}
|
|
|
- <div className="space-y-2">
|
|
|
- <Label htmlFor="state">状态</Label>
|
|
|
- <Select
|
|
|
- value={formData.state}
|
|
|
- onValueChange={(value) => handleInputChange('state', value)}
|
|
|
+ {/* 操作按钮 */}
|
|
|
+ <div className="flex justify-end gap-2 pt-4">
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="outline"
|
|
|
+ onClick={onCancel}
|
|
|
+ disabled={isLoading}
|
|
|
>
|
|
|
- <SelectTrigger className={errors.state ? 'border-destructive' : ''}>
|
|
|
- <SelectValue placeholder="选择状态" />
|
|
|
- </SelectTrigger>
|
|
|
- <SelectContent>
|
|
|
- <SelectItem value="1">可用</SelectItem>
|
|
|
- <SelectItem value="2">不可用</SelectItem>
|
|
|
- </SelectContent>
|
|
|
- </Select>
|
|
|
- {errors.state && (
|
|
|
- <p className="text-sm text-destructive">{errors.state}</p>
|
|
|
- )}
|
|
|
+ <X className="h-4 w-4 mr-2" />
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ <Button type="submit" disabled={isLoading}>
|
|
|
+ <Save className="h-4 w-4 mr-2" />
|
|
|
+ {isLoading ? '保存中...' : '保存'}
|
|
|
+ </Button>
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 操作按钮 */}
|
|
|
- <div className="flex justify-end gap-2 pt-4">
|
|
|
- <Button
|
|
|
- type="button"
|
|
|
- variant="outline"
|
|
|
- onClick={onCancel}
|
|
|
- disabled={isLoading}
|
|
|
- >
|
|
|
- <X className="h-4 w-4 mr-2" />
|
|
|
- 取消
|
|
|
- </Button>
|
|
|
- <Button type="submit" disabled={isLoading}>
|
|
|
- <Save className="h-4 w-4 mr-2" />
|
|
|
- {isLoading ? '保存中...' : '保存'}
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- </form>
|
|
|
+ </form>
|
|
|
+ </Form>
|
|
|
);
|
|
|
};
|