229 lines
8.4 KiB
TypeScript
229 lines
8.4 KiB
TypeScript
import { Button } from '@/components/ui/button';
|
||
import { Form } from '@/components/ui/form';
|
||
import { zodResolver } from '@hookform/resolvers/zod';
|
||
import React, { useEffect, useRef, useState } from 'react';
|
||
import { useForm } from 'react-hook-form';
|
||
import { z } from 'zod';
|
||
import ClothingCard from './components/ClothingCard';
|
||
import {
|
||
api,
|
||
type Body_async_cloud_change_bg_v3_api_v3_cloud_batch_change_bg_post,
|
||
type Body_async_cloud_gen_images_v3_api_v3_cloud_batch_edit_images_post,
|
||
type Body_local_cloud_async_change_bg_api_v2_cloud_batch_change_bg_post,
|
||
} from '@/api';
|
||
import { type BgPaddingMultiValue } from '@/components/block/BgPaddingMultiSelect';
|
||
|
||
// 五级联动的 tag schema
|
||
const tagSchema = z.object({
|
||
scenes: z.array(z.array(z.string())).optional(),
|
||
});
|
||
|
||
const formSchema = z.object({
|
||
clothing_images: z.array(z.instanceof(File)).min(1, '请上传至少一张服装图片').max(5, '最多上传5个商品'),
|
||
tags: z.array(tagSchema),
|
||
changeBg: z.boolean().optional(),
|
||
});
|
||
|
||
interface TagForm {
|
||
scenes?: string[][];
|
||
paddingList?: BgPaddingMultiValue;
|
||
bgMode?: 'custom' | 'scene';
|
||
}
|
||
|
||
interface FormValues {
|
||
clothing_images: File[];
|
||
tags: TagForm[];
|
||
changeBg?: boolean;
|
||
}
|
||
|
||
const defaultValues: FormValues = {
|
||
clothing_images: [],
|
||
tags: [],
|
||
changeBg: false,
|
||
};
|
||
|
||
const TryOnPage: React.FC = () => {
|
||
const [scenesOptions, setScenesOptions] = useState<any[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [result, setResult] = useState<any>(null);
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
||
|
||
// 表单
|
||
const form = useForm<FormValues>({
|
||
resolver: zodResolver(formSchema),
|
||
defaultValues,
|
||
});
|
||
|
||
// 拉取标签数据
|
||
useEffect(() => {
|
||
api.TagService.fetchStyleAvailableTagsApiTagStyleTagListGet().then(data => setScenesOptions(data.data || []));
|
||
}, []);
|
||
|
||
const tags = form.watch('tags');
|
||
|
||
// 图片上传和标签组同步(append 新卡片)
|
||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const files = Array.from(e.target.files || []);
|
||
if (!files.length) return;
|
||
const oldFiles = form.getValues('clothing_images');
|
||
if (oldFiles.length + files.length > 5) {
|
||
alert('最多只能上传5个商品');
|
||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||
return;
|
||
}
|
||
// 追加到已有图片和标签
|
||
const oldTags = form.getValues('tags');
|
||
const newFiles = [...oldFiles, ...files];
|
||
const newTags = [...oldTags, ...files.map(() => ({ scenes: [] as string[][] }))];
|
||
form.setValue('clothing_images', newFiles);
|
||
setImagePreviews(newFiles.map(file => URL.createObjectURL(file)));
|
||
form.setValue('tags', newTags);
|
||
// 清空 input 以便连续选择同一图片
|
||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||
};
|
||
|
||
// 删除图片及对应标签组
|
||
const handleRemoveImage = (idx: number) => {
|
||
const files = form.getValues('clothing_images');
|
||
const tags = form.getValues('tags');
|
||
const newFiles = files.filter((_, i) => i !== idx);
|
||
const newTags = tags.filter((_, i) => i !== idx);
|
||
form.setValue('clothing_images', newFiles);
|
||
setImagePreviews(newFiles.map(file => URL.createObjectURL(file)));
|
||
form.setValue('tags', newTags);
|
||
};
|
||
|
||
// 场景多选
|
||
const handleScenesChange = (idx: number, scenes: string[][]) => {
|
||
const newTags = [...form.getValues('tags')];
|
||
newTags[idx] = { ...newTags[idx], scenes };
|
||
form.setValue('tags', newTags);
|
||
};
|
||
|
||
// 自定义上传
|
||
const handlePaddingListChange = (idx: number, paddingList: BgPaddingMultiValue) => {
|
||
const newTags = [...form.getValues('tags')];
|
||
newTags[idx] = { ...newTags[idx], paddingList };
|
||
form.setValue('tags', newTags);
|
||
};
|
||
|
||
// 切换背景模式
|
||
const handleBgModeChange = (idx: number, bgMode: 'custom' | 'scene') => {
|
||
const newTags = [...form.getValues('tags')];
|
||
newTags[idx] = { ...newTags[idx], bgMode };
|
||
form.setValue('tags', newTags);
|
||
};
|
||
|
||
// 递归查找 prompt
|
||
function findPromptByPath(tree: any[], path: string[]): string {
|
||
let node: any = null;
|
||
let nodes: any[] = tree;
|
||
for (const t of path) {
|
||
node = nodes.find((n: any) => n.title === t);
|
||
if (!node) return '';
|
||
nodes = node.children || [];
|
||
}
|
||
return node && node.prompt ? node.prompt : '';
|
||
}
|
||
|
||
// 提交
|
||
const onSubmit = async (values: FormValues) => {
|
||
setLoading(true);
|
||
setResult(null);
|
||
const results: any[] = [];
|
||
console.log(values);
|
||
try {
|
||
for (let i = 0; i < values.clothing_images.length; i++) {
|
||
const tag = values.tags[i];
|
||
const bgMode = tag.bgMode || 'scene';
|
||
if (bgMode === 'custom') {
|
||
// 自定义模式
|
||
const paddingList = tag.paddingList || [];
|
||
for (const padding of paddingList) {
|
||
const formData: Body_async_cloud_change_bg_v3_api_v3_cloud_batch_change_bg_post = {
|
||
clothing_images: [values.clothing_images[i]],
|
||
padding_images: [padding.file],
|
||
bg_prompts: padding.text,
|
||
};
|
||
const res = await api.ImageGenerateService.asyncCloudChangeBgV3ApiV3CloudBatchChangeBgPost({ formData });
|
||
results.push(res);
|
||
}
|
||
} else {
|
||
// 场景模式
|
||
const scenesArr: string[][] = tag.scenes || [];
|
||
for (const path of scenesArr) {
|
||
const scene = {
|
||
title: path.join('/'),
|
||
prompt: findPromptByPath(scenesOptions, path),
|
||
};
|
||
const formData: Body_local_cloud_async_change_bg_api_v2_cloud_batch_change_bg_post = {
|
||
clothing_images: [values.clothing_images[i]],
|
||
scenes_list: JSON.stringify([{ title: scene.title, prompt: scene.prompt }]),
|
||
};
|
||
const res = await api.ImageGenerateService.localCloudAsyncChangeBgApiV2CloudBatchChangeBgPost({ formData });
|
||
results.push(res);
|
||
}
|
||
}
|
||
}
|
||
setResult(results);
|
||
} catch (e: any) {
|
||
setResult({ error: e?.message || '请求失败' });
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const clothing_images = form.watch('clothing_images');
|
||
|
||
return (
|
||
<div className='p-6 max-w-4xl mx-auto'>
|
||
<h1 className='text-2xl font-bold mb-6'>换装体验</h1>
|
||
<Form {...form}>
|
||
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
|
||
{/* 卡片式图片+表单 */}
|
||
<div className='flex flex-col gap-8'>
|
||
{clothing_images &&
|
||
clothing_images.length > 0 &&
|
||
clothing_images.map((_, idx) => {
|
||
const tag: TagForm = tags[idx] || { scenes: [] };
|
||
const fileName = clothing_images[idx]?.name;
|
||
return (
|
||
<ClothingCard
|
||
key={idx}
|
||
imageUrl={imagePreviews[idx]}
|
||
onRemove={() => handleRemoveImage(idx)}
|
||
scenesOptions={scenesOptions}
|
||
scenes={tag.scenes || []}
|
||
onScenesChange={scenes => handleScenesChange(idx, scenes)}
|
||
fileName={fileName}
|
||
paddingList={tag.paddingList || []}
|
||
onPaddingListChange={paddingList => handlePaddingListChange(idx, paddingList)}
|
||
bgMode={tag.bgMode || 'scene'}
|
||
onChangeBgMode={bgMode => handleBgModeChange(idx, bgMode)}
|
||
/>
|
||
);
|
||
})}
|
||
<Button type='button' onClick={() => fileInputRef.current?.click()} className='w-40 self-center' disabled={clothing_images.length >= 5}>
|
||
添加商品
|
||
</Button>
|
||
<input ref={fileInputRef} type='file' accept='image/*' multiple className='hidden' onChange={handleImageChange} />
|
||
</div>
|
||
<Button type='submit' disabled={loading} className='w-full'>
|
||
{loading ? '提交中...' : '提交换背景'}
|
||
</Button>
|
||
</form>
|
||
</Form>
|
||
{/* 结果展示 */}
|
||
{result && (
|
||
<div className='mt-8'>
|
||
<h2 className='text-lg font-semibold mb-2'>结果</h2>
|
||
<pre className='bg-muted p-4 rounded text-sm overflow-x-auto'>{JSON.stringify(result, null, 2)}</pre>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default TryOnPage;
|