glam-web/src/pages/ChangeBgPage/index.tsx

229 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;