327 lines
13 KiB
TypeScript
327 lines
13 KiB
TypeScript
import React, { useEffect, useMemo } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
|
|
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form';
|
|
import { useForm } from 'react-hook-form';
|
|
import { z } from 'zod';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { api } from '@/api';
|
|
|
|
interface ModelFormProps {
|
|
initialData?: any;
|
|
onSubmit: (data: any, id?: number) => void;
|
|
onCancel: () => void;
|
|
editMode?: boolean;
|
|
}
|
|
|
|
const formSchema = z.object({
|
|
sex: z.string().min(1, '请选择性别'),
|
|
category: z.string().min(1, '请选择类别'),
|
|
size: z.string().min(1, '请选择尺寸'),
|
|
material: z.string().min(1, '请选择材质'),
|
|
color: z.string().min(1, '请选择颜色'),
|
|
cover: z.any().optional(),
|
|
status: z.boolean(),
|
|
});
|
|
|
|
type FormValues = z.infer<typeof formSchema>;
|
|
|
|
const defaultValues: FormValues = {
|
|
sex: '',
|
|
category: '',
|
|
size: '',
|
|
material: '',
|
|
color: '',
|
|
cover: undefined,
|
|
status: true,
|
|
};
|
|
|
|
const ModelForm: React.FC<ModelFormProps> = ({ initialData, onSubmit, onCancel, editMode }) => {
|
|
const [tagOptions, setTagOptions] = React.useState<any[]>([]);
|
|
const [coverPreview, setCoverPreview] = React.useState<string | null>(null);
|
|
// 新增:用于触发文件选择的 ref
|
|
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
// 初始化表单
|
|
const form = useForm<FormValues>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: initialData
|
|
? {
|
|
sex: initialData.tag?.sex || '',
|
|
category: initialData.tag?.category || '',
|
|
size: initialData.tag?.size || '',
|
|
material: initialData.tag?.material || '',
|
|
color: initialData.tag?.color || '',
|
|
cover: undefined,
|
|
status: initialData.status === 1 ? true : false,
|
|
}
|
|
: defaultValues,
|
|
});
|
|
|
|
// 支持初始图片预览
|
|
React.useEffect(() => {
|
|
if (initialData?.coverUrl) {
|
|
setCoverPreview(initialData.coverUrl);
|
|
}
|
|
}, [initialData]);
|
|
|
|
// 级联选择逻辑
|
|
const sexOptions = tagOptions;
|
|
const categoryOptions = useMemo(() => {
|
|
return form.watch('sex') ? tagOptions.find((t: any) => t.title === form.watch('sex'))?.children || [] : [];
|
|
}, [tagOptions, form.watch('sex')]);
|
|
const sizeOptions = useMemo(() => {
|
|
return form.watch('category') ? categoryOptions.find((t: any) => t.title === form.watch('category'))?.children || [] : [];
|
|
}, [categoryOptions, form.watch('category')]);
|
|
const materialOptions = useMemo(() => {
|
|
if (!form.watch('size')) return [];
|
|
const size = sizeOptions.find((t: any) => t.title === form.watch('size'));
|
|
return size?.children?.find((c: any) => c.title === '材质')?.children || [];
|
|
}, [sizeOptions, form.watch('size')]);
|
|
const colorOptions = useMemo(() => {
|
|
if (!form.watch('size')) return [];
|
|
const size = sizeOptions.find((t: any) => t.title === form.watch('size'));
|
|
return size?.children?.find((c: any) => c.title === '颜色')?.children || [];
|
|
}, [sizeOptions, form.watch('size')]);
|
|
|
|
// 拉取标签数据
|
|
useEffect(() => {
|
|
api.TagService.fetchModelTagsApiTagModelTagListGet().then(data => setTagOptions(data.data || []));
|
|
}, []);
|
|
|
|
// 提交
|
|
const handleFormSubmit = async (values: FormValues) => {
|
|
const tagString = [values.sex, values.category, values.size, values.material, values.color].join('_');
|
|
onSubmit({ tags: tagString, cover_image: values.cover, status: values.status ? 1 : 0 }, initialData?.id);
|
|
};
|
|
|
|
// 级联联动清空下级
|
|
const handleCascade = (level: keyof FormValues, value: string) => {
|
|
if (level === 'sex') {
|
|
form.setValue('sex', value);
|
|
form.setValue('category', '');
|
|
form.setValue('size', '');
|
|
form.setValue('material', '');
|
|
form.setValue('color', '');
|
|
} else if (level === 'category') {
|
|
form.setValue('category', value);
|
|
form.setValue('size', '');
|
|
form.setValue('material', '');
|
|
form.setValue('color', '');
|
|
} else if (level === 'size') {
|
|
form.setValue('size', value);
|
|
form.setValue('material', '');
|
|
form.setValue('color', '');
|
|
} else if (level === 'material') {
|
|
form.setValue('material', value);
|
|
} else if (level === 'color') {
|
|
form.setValue('color', value);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(handleFormSubmit)} className='flex flex-col md:flex-row gap-8 items-start'>
|
|
{/* 左侧图片区,点击可上传 */}
|
|
<div className='w-full md:w-[240px] flex-shrink-0 flex flex-col items-center'>
|
|
<div
|
|
className='w-full aspect-[9/16] bg-muted flex items-center justify-center rounded-lg border border-muted shadow-md overflow-hidden relative group cursor-pointer'
|
|
onClick={() => fileInputRef.current?.click()}
|
|
tabIndex={0}
|
|
role='button'
|
|
aria-label='上传封面图片'
|
|
>
|
|
{coverPreview ? (
|
|
<img
|
|
src={coverPreview}
|
|
alt='预览'
|
|
className='object-contain w-full h-full transition-transform duration-200 hover:scale-105 bg-white'
|
|
style={{ background: '#f8f8f8' }}
|
|
/>
|
|
) : (
|
|
<span className='text-muted-foreground text-sm'>暂无图片</span>
|
|
)}
|
|
{/* 遮罩/按钮 */}
|
|
<div className='absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity pointer-events-none'>
|
|
<span className='text-white text-base font-medium pointer-events-none'>点击上传封面</span>
|
|
</div>
|
|
{/* 隐藏的文件选择 */}
|
|
<input
|
|
ref={fileInputRef}
|
|
type='file'
|
|
accept='image/*'
|
|
className='hidden'
|
|
onChange={e => {
|
|
const file = e.target.files?.[0];
|
|
form.setValue('cover', file);
|
|
if (file) {
|
|
setCoverPreview(URL.createObjectURL(file));
|
|
} else {
|
|
setCoverPreview(null);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
{/* 错误提示 */}
|
|
<div className='mt-2 w-full'>
|
|
<FormField control={form.control} name='cover' render={() => <FormMessage />} />
|
|
</div>
|
|
</div>
|
|
{/* 右侧表单区(去除封面图片项) */}
|
|
<div className='flex-1 w-full flex flex-col gap-4'>
|
|
{/* 性别 */}
|
|
<FormField
|
|
control={form.control}
|
|
name='sex'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>性别</FormLabel>
|
|
<FormControl>
|
|
<Select value={field.value} onValueChange={v => handleCascade('sex', v)}>
|
|
<SelectTrigger className='w-full'>
|
|
<SelectValue placeholder='请选择性别' />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sexOptions.map((item: any) => (
|
|
<SelectItem key={item.title} value={item.title}>
|
|
{item.title}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
{/* 类别 */}
|
|
<FormField
|
|
control={form.control}
|
|
name='category'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>类别</FormLabel>
|
|
<FormControl>
|
|
<Select value={field.value} onValueChange={v => handleCascade('category', v)} disabled={!form.watch('sex')}>
|
|
<SelectTrigger className='w-full'>
|
|
<SelectValue placeholder='请选择类别' />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{categoryOptions.map((item: any) => (
|
|
<SelectItem key={item.title} value={item.title}>
|
|
{item.title}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
{/* 尺寸 */}
|
|
<FormField
|
|
control={form.control}
|
|
name='size'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>尺寸</FormLabel>
|
|
<FormControl>
|
|
<Select value={field.value} onValueChange={v => handleCascade('size', v)} disabled={!form.watch('category')}>
|
|
<SelectTrigger className='w-full'>
|
|
<SelectValue placeholder='请选择尺寸' />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sizeOptions.map((item: any) => (
|
|
<SelectItem key={item.title} value={item.title}>
|
|
{item.title}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
{/* 材质 */}
|
|
<FormField
|
|
control={form.control}
|
|
name='material'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>材质</FormLabel>
|
|
<FormControl>
|
|
<Select value={field.value} onValueChange={v => handleCascade('material', v)} disabled={!form.watch('size')}>
|
|
<SelectTrigger className='w-full'>
|
|
<SelectValue placeholder='请选择材质' />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{materialOptions.map((item: any) => (
|
|
<SelectItem key={item.title} value={item.title}>
|
|
{item.title}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
{/* 颜色 */}
|
|
<FormField
|
|
control={form.control}
|
|
name='color'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>颜色</FormLabel>
|
|
<FormControl>
|
|
<Select value={field.value} onValueChange={v => handleCascade('color', v)} disabled={!form.watch('size')}>
|
|
<SelectTrigger className='w-full'>
|
|
<SelectValue placeholder='请选择颜色' />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{colorOptions.map((item: any) => (
|
|
<SelectItem key={item.title} value={item.title}>
|
|
{item.title}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
{/* 启用 */}
|
|
<FormField
|
|
control={form.control}
|
|
name='status'
|
|
render={({ field }) => (
|
|
<FormItem className='flex items-center space-x-2'>
|
|
<FormLabel htmlFor='status'>启用</FormLabel>
|
|
<FormControl>
|
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<div className='flex justify-end space-x-2 mt-6'>
|
|
<Button type='button' variant='outline' onClick={onCancel} disabled={form.formState.isSubmitting}>
|
|
取消
|
|
</Button>
|
|
<Button type='submit' disabled={form.formState.isSubmitting}>
|
|
{form.formState.isSubmitting ? '加载中...' : editMode ? '保存' : '创建'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
);
|
|
};
|
|
|
|
export default ModelForm;
|