Files
glam-web/src/pages/ModelPage/components/ModelForm.tsx
2025-07-02 13:51:09 +08:00

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;