feat: 完善
This commit is contained in:
@@ -3,6 +3,7 @@ import { Sidebar, SidebarContent, SidebarMenu, SidebarMenuButton, SidebarMenuIte
|
||||
import { Home, Users } from 'lucide-react';
|
||||
import { Link, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import ModelPage from './pages/ModelPage';
|
||||
import TryOnPage from './pages/TryOnPage';
|
||||
|
||||
const menu = [
|
||||
{ path: '/', label: '首页', icon: <Home className='mr-2' /> },
|
||||
@@ -33,7 +34,7 @@ function App() {
|
||||
</Sidebar>
|
||||
<main className='flex-1 p-8'>
|
||||
<Routes>
|
||||
<Route path='/' element={<div>首页内容</div>} />
|
||||
<Route path='/' element={<TryOnPage />} />
|
||||
<Route path='/models' element={<ModelPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
@@ -8,19 +8,15 @@ export { OpenAPI } from './core/OpenAPI';
|
||||
export type { OpenAPIConfig } from './core/OpenAPI';
|
||||
|
||||
export type { BatchImageEditData } from './models/BatchImageEditData';
|
||||
export type { BatchVideoGenerateRequest } from './models/BatchVideoGenerateRequest';
|
||||
export type { BatchVideoTaskRequest } from './models/BatchVideoTaskRequest';
|
||||
export type { Body_async_custom_edit_image_api_async_custom_image_post } from './models/Body_async_custom_edit_image_api_async_custom_image_post';
|
||||
export type { Body_background_custom_edit_image_api_background_custom_image_post } from './models/Body_background_custom_edit_image_api_background_custom_image_post';
|
||||
export type { Body_create_model_api_models_create_post } from './models/Body_create_model_api_models_create_post';
|
||||
export type { Body_handler_submit_task_api_comfy_async_submit_task_post } from './models/Body_handler_submit_task_api_comfy_async_submit_task_post';
|
||||
export type { Body_local_async_batch_bg_opt_api_local_batch_change_bg_post } from './models/Body_local_async_batch_bg_opt_api_local_batch_change_bg_post';
|
||||
export type { Body_local_async_change_bg_api_local_change_bg_post } from './models/Body_local_async_change_bg_api_local_change_bg_post';
|
||||
export type { Body_local_async_change_bg_api_v2_local_batch_change_bg_post } from './models/Body_local_async_change_bg_api_v2_local_batch_change_bg_post';
|
||||
export type { Body_local_async_gen_images_api_v2_local_batch_edit_images_post } from './models/Body_local_async_gen_images_api_v2_local_batch_edit_images_post';
|
||||
export type { Body_local_async_gen_images_api_v2_local_batch_images_post } from './models/Body_local_async_gen_images_api_v2_local_batch_images_post';
|
||||
export type { Body_local_sync_generate_image_api_local_batch_generate_image_post } from './models/Body_local_sync_generate_image_api_local_batch_generate_image_post';
|
||||
export type { Body_opt_local_generate_image_api_local_batch_generate_image_latest_post } from './models/Body_opt_local_generate_image_api_local_batch_generate_image_latest_post';
|
||||
export type { Body_sync_custom_edit_image_api_sync_custom_image_post } from './models/Body_sync_custom_edit_image_api_sync_custom_image_post';
|
||||
export type { Body_update_model_api_models__model_id__put } from './models/Body_update_model_api_models__model_id__put';
|
||||
export type { Body_upload_image_api_upload_post } from './models/Body_upload_image_api_upload_post';
|
||||
export type { HTTPValidationError } from './models/HTTPValidationError';
|
||||
@@ -28,24 +24,19 @@ export type { ImageEditItem } from './models/ImageEditItem';
|
||||
export type { TaskQueryRequest } from './models/TaskQueryRequest';
|
||||
export type { UploadResponse } from './models/UploadResponse';
|
||||
export type { ValidationError } from './models/ValidationError';
|
||||
export type { VideoGenerateRequest } from './models/VideoGenerateRequest';
|
||||
export type { VideoTaskRequest } from './models/VideoTaskRequest';
|
||||
export type { VideoTaskStatus } from './models/VideoTaskStatus';
|
||||
|
||||
export { $BatchImageEditData } from './schemas/$BatchImageEditData';
|
||||
export { $BatchVideoGenerateRequest } from './schemas/$BatchVideoGenerateRequest';
|
||||
export { $BatchVideoTaskRequest } from './schemas/$BatchVideoTaskRequest';
|
||||
export { $Body_async_custom_edit_image_api_async_custom_image_post } from './schemas/$Body_async_custom_edit_image_api_async_custom_image_post';
|
||||
export { $Body_background_custom_edit_image_api_background_custom_image_post } from './schemas/$Body_background_custom_edit_image_api_background_custom_image_post';
|
||||
export { $Body_create_model_api_models_create_post } from './schemas/$Body_create_model_api_models_create_post';
|
||||
export { $Body_handler_submit_task_api_comfy_async_submit_task_post } from './schemas/$Body_handler_submit_task_api_comfy_async_submit_task_post';
|
||||
export { $Body_local_async_batch_bg_opt_api_local_batch_change_bg_post } from './schemas/$Body_local_async_batch_bg_opt_api_local_batch_change_bg_post';
|
||||
export { $Body_local_async_change_bg_api_local_change_bg_post } from './schemas/$Body_local_async_change_bg_api_local_change_bg_post';
|
||||
export { $Body_local_async_change_bg_api_v2_local_batch_change_bg_post } from './schemas/$Body_local_async_change_bg_api_v2_local_batch_change_bg_post';
|
||||
export { $Body_local_async_gen_images_api_v2_local_batch_edit_images_post } from './schemas/$Body_local_async_gen_images_api_v2_local_batch_edit_images_post';
|
||||
export { $Body_local_async_gen_images_api_v2_local_batch_images_post } from './schemas/$Body_local_async_gen_images_api_v2_local_batch_images_post';
|
||||
export { $Body_local_sync_generate_image_api_local_batch_generate_image_post } from './schemas/$Body_local_sync_generate_image_api_local_batch_generate_image_post';
|
||||
export { $Body_opt_local_generate_image_api_local_batch_generate_image_latest_post } from './schemas/$Body_opt_local_generate_image_api_local_batch_generate_image_latest_post';
|
||||
export { $Body_sync_custom_edit_image_api_sync_custom_image_post } from './schemas/$Body_sync_custom_edit_image_api_sync_custom_image_post';
|
||||
export { $Body_update_model_api_models__model_id__put } from './schemas/$Body_update_model_api_models__model_id__put';
|
||||
export { $Body_upload_image_api_upload_post } from './schemas/$Body_upload_image_api_upload_post';
|
||||
export { $HTTPValidationError } from './schemas/$HTTPValidationError';
|
||||
@@ -53,7 +44,6 @@ export { $ImageEditItem } from './schemas/$ImageEditItem';
|
||||
export { $TaskQueryRequest } from './schemas/$TaskQueryRequest';
|
||||
export { $UploadResponse } from './schemas/$UploadResponse';
|
||||
export { $ValidationError } from './schemas/$ValidationError';
|
||||
export { $VideoGenerateRequest } from './schemas/$VideoGenerateRequest';
|
||||
export { $VideoTaskRequest } from './schemas/$VideoTaskRequest';
|
||||
export { $VideoTaskStatus } from './schemas/$VideoTaskStatus';
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { VideoGenerateRequest } from './VideoGenerateRequest';
|
||||
export type BatchVideoGenerateRequest = {
|
||||
data: Array<VideoGenerateRequest>;
|
||||
};
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Body_background_custom_edit_image_api_background_custom_image_post = {
|
||||
img_path: Blob;
|
||||
};
|
||||
|
||||
@@ -5,12 +5,8 @@
|
||||
export type Body_local_async_change_bg_api_v2_local_batch_change_bg_post = {
|
||||
clothing_images: Array<Blob>;
|
||||
/**
|
||||
* 风格标签
|
||||
* 场景标签集合JSON字符串
|
||||
*/
|
||||
style: string;
|
||||
/**
|
||||
* 场景标签,支持多个标签,标签之间使用逗号分割
|
||||
*/
|
||||
scenes: string;
|
||||
scenes_tags: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Body_local_async_gen_images_api_v2_local_batch_edit_images_post = {
|
||||
clothing_images: Array<Blob>;
|
||||
/**
|
||||
* 标签列表JSON字符串,格式:["男_上衣_长款_牛仔_红", "xxxx"]
|
||||
*/
|
||||
tag_list: string;
|
||||
/**
|
||||
* 场景标签JSON字符串
|
||||
*/
|
||||
scenes_list: string;
|
||||
/**
|
||||
* 操作模式,支持 both, edit_only, bg_only
|
||||
*/
|
||||
mode?: string;
|
||||
};
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Body_local_sync_generate_image_api_local_batch_generate_image_post = {
|
||||
clothing_images: Array<Blob>;
|
||||
model_names: string;
|
||||
model_category: string;
|
||||
mode?: string;
|
||||
clothing_dec_prompt?: string;
|
||||
};
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Body_opt_local_generate_image_api_local_batch_generate_image_latest_post = {
|
||||
clothing_images: Array<Blob>;
|
||||
model_url: string;
|
||||
mode?: string;
|
||||
model_category: string;
|
||||
clothing_dec_prompt?: string;
|
||||
scenario_categories?: Array<string>;
|
||||
};
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Body_sync_custom_edit_image_api_sync_custom_image_post = {
|
||||
img_path: Blob;
|
||||
};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type VideoGenerateRequest = {
|
||||
img_url: string;
|
||||
prompt: string;
|
||||
duration?: string;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
export * from './BatchImageEditData';
|
||||
export * from './BatchVideoGenerateRequest';
|
||||
export * from './BatchVideoTaskRequest';
|
||||
export * from './Body_async_custom_edit_image_api_async_custom_image_post';
|
||||
export * from './Body_background_custom_edit_image_api_background_custom_image_post';
|
||||
export * from './Body_create_model_api_models_create_post';
|
||||
export * from './Body_handler_submit_task_api_comfy_async_submit_task_post';
|
||||
export * from './Body_local_async_batch_bg_opt_api_local_batch_change_bg_post';
|
||||
export * from './Body_local_async_change_bg_api_local_change_bg_post';
|
||||
export * from './Body_local_async_change_bg_api_v2_local_batch_change_bg_post';
|
||||
export * from './Body_local_async_gen_images_api_v2_local_batch_edit_images_post';
|
||||
export * from './Body_local_async_gen_images_api_v2_local_batch_images_post';
|
||||
export * from './Body_local_sync_generate_image_api_local_batch_generate_image_post';
|
||||
export * from './Body_opt_local_generate_image_api_local_batch_generate_image_latest_post';
|
||||
export * from './Body_sync_custom_edit_image_api_sync_custom_image_post';
|
||||
export * from './Body_update_model_api_models__model_id__put';
|
||||
export * from './Body_upload_image_api_upload_post';
|
||||
export * from './HTTPValidationError';
|
||||
@@ -19,6 +15,5 @@ export * from './ImageEditItem';
|
||||
export * from './TaskQueryRequest';
|
||||
export * from './UploadResponse';
|
||||
export * from './ValidationError';
|
||||
export * from './VideoGenerateRequest';
|
||||
export * from './VideoTaskRequest';
|
||||
export * from './VideoTaskStatus';
|
||||
@@ -1,15 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const $BatchVideoGenerateRequest = {
|
||||
properties: {
|
||||
data: {
|
||||
type: 'array',
|
||||
contains: {
|
||||
type: 'VideoGenerateRequest',
|
||||
},
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -1,13 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const $Body_background_custom_edit_image_api_background_custom_image_post = {
|
||||
properties: {
|
||||
img_path: {
|
||||
type: 'binary',
|
||||
isRequired: true,
|
||||
format: 'binary',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -12,14 +12,9 @@ export const $Body_local_async_change_bg_api_v2_local_batch_change_bg_post = {
|
||||
},
|
||||
isRequired: true,
|
||||
},
|
||||
style: {
|
||||
scenes_tags: {
|
||||
type: 'string',
|
||||
description: `风格标签`,
|
||||
isRequired: true,
|
||||
},
|
||||
scenes: {
|
||||
type: 'string',
|
||||
description: `场景标签,支持多个标签,标签之间使用逗号分割`,
|
||||
description: `场景标签集合JSON字符串`,
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const $Body_local_sync_generate_image_api_local_batch_generate_image_post = {
|
||||
export const $Body_local_async_gen_images_api_v2_local_batch_edit_images_post = {
|
||||
properties: {
|
||||
clothing_images: {
|
||||
type: 'array',
|
||||
@@ -12,19 +12,19 @@ export const $Body_local_sync_generate_image_api_local_batch_generate_image_post
|
||||
},
|
||||
isRequired: true,
|
||||
},
|
||||
model_names: {
|
||||
tag_list: {
|
||||
type: 'string',
|
||||
description: `标签列表JSON字符串,格式:["男_上衣_长款_牛仔_红", "xxxx"]`,
|
||||
isRequired: true,
|
||||
},
|
||||
model_category: {
|
||||
scenes_list: {
|
||||
type: 'string',
|
||||
description: `场景标签JSON字符串`,
|
||||
isRequired: true,
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
},
|
||||
clothing_dec_prompt: {
|
||||
type: 'string',
|
||||
description: `操作模式,支持 both, edit_only, bg_only`,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -1,36 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const $Body_opt_local_generate_image_api_local_batch_generate_image_latest_post = {
|
||||
properties: {
|
||||
clothing_images: {
|
||||
type: 'array',
|
||||
contains: {
|
||||
type: 'binary',
|
||||
format: 'binary',
|
||||
},
|
||||
isRequired: true,
|
||||
},
|
||||
model_url: {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
},
|
||||
model_category: {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
},
|
||||
clothing_dec_prompt: {
|
||||
type: 'string',
|
||||
},
|
||||
scenario_categories: {
|
||||
type: 'array',
|
||||
contains: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -1,13 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const $Body_sync_custom_edit_image_api_sync_custom_image_post = {
|
||||
properties: {
|
||||
img_path: {
|
||||
type: 'binary',
|
||||
isRequired: true,
|
||||
format: 'binary',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -1,19 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const $VideoGenerateRequest = {
|
||||
properties: {
|
||||
img_url: {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
},
|
||||
duration: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -3,14 +3,11 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { Body_async_custom_edit_image_api_async_custom_image_post } from '../models/Body_async_custom_edit_image_api_async_custom_image_post';
|
||||
import type { Body_background_custom_edit_image_api_background_custom_image_post } from '../models/Body_background_custom_edit_image_api_background_custom_image_post';
|
||||
import type { Body_local_async_batch_bg_opt_api_local_batch_change_bg_post } from '../models/Body_local_async_batch_bg_opt_api_local_batch_change_bg_post';
|
||||
import type { Body_local_async_change_bg_api_local_change_bg_post } from '../models/Body_local_async_change_bg_api_local_change_bg_post';
|
||||
import type { Body_local_async_change_bg_api_v2_local_batch_change_bg_post } from '../models/Body_local_async_change_bg_api_v2_local_batch_change_bg_post';
|
||||
import type { Body_local_async_gen_images_api_v2_local_batch_edit_images_post } from '../models/Body_local_async_gen_images_api_v2_local_batch_edit_images_post';
|
||||
import type { Body_local_async_gen_images_api_v2_local_batch_images_post } from '../models/Body_local_async_gen_images_api_v2_local_batch_images_post';
|
||||
import type { Body_local_sync_generate_image_api_local_batch_generate_image_post } from '../models/Body_local_sync_generate_image_api_local_batch_generate_image_post';
|
||||
import type { Body_opt_local_generate_image_api_local_batch_generate_image_latest_post } from '../models/Body_opt_local_generate_image_api_local_batch_generate_image_latest_post';
|
||||
import type { Body_sync_custom_edit_image_api_sync_custom_image_post } from '../models/Body_sync_custom_edit_image_api_sync_custom_image_post';
|
||||
import type { Body_upload_image_api_upload_post } from '../models/Body_upload_image_api_upload_post';
|
||||
import type { TaskQueryRequest } from '../models/TaskQueryRequest';
|
||||
import type { UploadResponse } from '../models/UploadResponse';
|
||||
@@ -18,17 +15,6 @@ import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
export class DefaultService {
|
||||
/**
|
||||
* Index
|
||||
* @returns string Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static indexGet(): CancelablePromise<string> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Upload Image
|
||||
* @returns UploadResponse Successful Response
|
||||
@@ -50,6 +36,7 @@ export class DefaultService {
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @deprecated
|
||||
* 本地换背景
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
@@ -70,6 +57,7 @@ export class DefaultService {
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @deprecated
|
||||
* 本地批量换背景图片
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
@@ -90,26 +78,7 @@ export class DefaultService {
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 自定义图片生成
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static syncCustomEditImageApiSyncCustomImagePost({
|
||||
formData,
|
||||
}: {
|
||||
formData: Body_sync_custom_edit_image_api_sync_custom_image_post,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/sync/custom/image',
|
||||
formData: formData,
|
||||
mediaType: 'multipart/form-data',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @deprecated
|
||||
* 异步自定义图片生成 - 立即返回task_id
|
||||
* 异步版本:立即返回task_id,避免阻塞worker
|
||||
* 客户端需要用task_id轮询 /api/check_task 查询结果
|
||||
@@ -132,81 +101,7 @@ export class DefaultService {
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 后台任务版本 - 使用BackgroundTasks
|
||||
* 后台任务版本:立即返回,任务在后台执行
|
||||
* 推荐用于不需要立即获取结果的场景
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static backgroundCustomEditImageApiBackgroundCustomImagePost({
|
||||
formData,
|
||||
}: {
|
||||
formData: Body_background_custom_edit_image_api_background_custom_image_post,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/background/custom/image',
|
||||
formData: formData,
|
||||
mediaType: 'multipart/form-data',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 本地异步生成图片 - 支持多个服装图片批处理
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static localSyncGenerateImageApiLocalBatchGenerateImagePost({
|
||||
formData,
|
||||
}: {
|
||||
formData: Body_local_sync_generate_image_api_local_batch_generate_image_post,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/local/batch/generate/image',
|
||||
formData: formData,
|
||||
mediaType: 'multipart/form-data',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 新版 异步图片生成接口, 支持多个服装图片处理
|
||||
* 新版异步图片生成接口 - 支持多个服装图片文件上传处理
|
||||
*
|
||||
* Args:
|
||||
* clothing_images: 服装图片文件列表
|
||||
* model_url: 模特图片URL
|
||||
* mode: 处理模式(both/edit_only/bg_only)
|
||||
* model_category: 模特分类
|
||||
* clothing_dec_prompt: 服装描述提示词
|
||||
* scenario_categories: 场景分类列表(暂未使用)
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 异步任务提交结果
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static optLocalGenerateImageApiLocalBatchGenerateImageLatestPost({
|
||||
formData,
|
||||
}: {
|
||||
formData: Body_opt_local_generate_image_api_local_batch_generate_image_latest_post,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/local/batch/generate/image/latest',
|
||||
formData: formData,
|
||||
mediaType: 'multipart/form-data',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @deprecated
|
||||
* 本地新版异步图片生成接口
|
||||
* 本地新版异步图片生成接口
|
||||
*
|
||||
@@ -238,6 +133,37 @@ export class DefaultService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 本地新版异步图片生成接口
|
||||
* 本地新版异步图片生成接口
|
||||
*
|
||||
* Args:
|
||||
* clothing_images: 服装图片文件列表
|
||||
* tag_list: 标签列表JSON字符串,格式:["男_上衣_长款_牛仔_红", "xxxx"] 注意tag_list 与 clothing_images 长度一致
|
||||
* scenes_list: 场景标签JSON字符串,格式:[{"title": "客厅1","prompt": "室内场景..."}]
|
||||
* mode: 操作模式(both/edit_only/bg_only)
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 异步任务提交结果
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static localAsyncGenImagesApiV2LocalBatchEditImagesPost({
|
||||
formData,
|
||||
}: {
|
||||
formData: Body_local_async_gen_images_api_v2_local_batch_edit_images_post,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/v2/local/batch/edit/images',
|
||||
formData: formData,
|
||||
mediaType: 'multipart/form-data',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 本地新版异步换背景接口
|
||||
* @returns any Successful Response
|
||||
@@ -263,14 +189,14 @@ export class DefaultService {
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static localSyncQueryTaskResultApiLocalBatchQueryTaskResultPost({
|
||||
public static localSyncQueryTaskResultApiV2LocalBatchQueryTaskResultPost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: TaskQueryRequest,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/local/batch/query/task/result',
|
||||
url: '/api/v2/local/batch/query/task/result',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { BatchVideoGenerateRequest } from '../models/BatchVideoGenerateRequest';
|
||||
import type { BatchVideoTaskRequest } from '../models/BatchVideoTaskRequest';
|
||||
import type { Body_create_model_api_models_create_post } from '../models/Body_create_model_api_models_create_post';
|
||||
import type { Body_update_model_api_models__model_id__put } from '../models/Body_update_model_api_models__model_id__put';
|
||||
import type { VideoGenerateRequest } from '../models/VideoGenerateRequest';
|
||||
import type { VideoTaskStatus } from '../models/VideoTaskStatus';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
@@ -335,68 +333,4 @@ export class Service {
|
||||
url: '/api/tag/model/tag_list',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 极梦视频生成任务提交
|
||||
* 生成视频API
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static generateVideoApiVideoGeneratePost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: VideoGenerateRequest,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/video/generate',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 最大支持同时提交5个任务
|
||||
* 批量生成视频API - 并发处理,最多同时5个任务
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static batchGenerateVideoApiBatchVideoGeneratePost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: BatchVideoGenerateRequest,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/batch/video/generate',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 极梦视频生成进度查询
|
||||
* 查询视频生成状态API
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static queryVideoStatusApiVideoQueryTaskIdGet({
|
||||
taskId,
|
||||
}: {
|
||||
taskId: string,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/video/query/{task_id}',
|
||||
path: {
|
||||
'task_id': taskId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,38 +6,15 @@ import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
export class WebhookService {
|
||||
/**
|
||||
* 同步数据处理
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static webhookHandlerApiHandlerWebhookPost(): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/handler/webhook',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 异步数据处理
|
||||
* 异步Webhook处理接口 - 快速响应,后台处理
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static asyncWebhookHandlerApiAsyncHandlerWebhookPost(): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/async/handler/webhook',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 异步数据处理
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static asyncWebhookSlotApiAsyncWebhookReciverPost(): CancelablePromise<any> {
|
||||
public static asyncWebhookSlotApiAsyncWebhookReceiverPost(): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/async/webhook/reciver',
|
||||
url: '/api/async/webhook/receiver',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
131
src/pages/TryOnPage/components/CascaderMultiSelect.tsx
Normal file
131
src/pages/TryOnPage/components/CascaderMultiSelect.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface CascaderNode {
|
||||
title: string;
|
||||
children?: CascaderNode[];
|
||||
prompt?: string;
|
||||
}
|
||||
|
||||
interface CascaderMultiSelectProps {
|
||||
options: CascaderNode[];
|
||||
value: string[][]; // 每个已选项为路径数组,如 ['户外', '公园', '公园1']
|
||||
onChange: (value: string[][]) => void;
|
||||
}
|
||||
|
||||
const getLeafPaths = (tree: CascaderNode[], prefix: string[] = []): string[][] => {
|
||||
let result: string[][] = [];
|
||||
for (const node of tree) {
|
||||
const path = [...prefix, node.title];
|
||||
if (node.children && node.children.length > 0) {
|
||||
result = result.concat(getLeafPaths(node.children, path));
|
||||
} else {
|
||||
result.push(path);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const CascaderMultiSelect: React.FC<CascaderMultiSelectProps> = ({ options, value, onChange }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activePath, setActivePath] = useState<string[]>([]);
|
||||
|
||||
// 当前级的 options
|
||||
const getCurrentOptions = () => {
|
||||
let nodes = options;
|
||||
for (const t of activePath) {
|
||||
const node = nodes.find(n => n.title === t);
|
||||
if (!node || !node.children) return [];
|
||||
nodes = node.children;
|
||||
}
|
||||
return nodes;
|
||||
};
|
||||
|
||||
// 判断某路径是否已选
|
||||
const isSelected = (path: string[]) => value.some(v => v.join('/') === path.join('/'));
|
||||
|
||||
// 选中叶子节点
|
||||
const handleSelect = (path: string[]) => {
|
||||
if (!isSelected(path)) {
|
||||
onChange([...value, path]);
|
||||
} else {
|
||||
onChange(value.filter(v => v.join('/') !== path.join('/')));
|
||||
}
|
||||
};
|
||||
|
||||
// 删除 tag
|
||||
const handleRemove = (path: string[]) => {
|
||||
onChange(value.filter(v => v.join('/') !== path.join('/')));
|
||||
};
|
||||
|
||||
// 渲染 tag
|
||||
const renderTags = () => (
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{value.map(v => (
|
||||
<span key={v.join('/')} className='bg-primary/10 text-primary px-2 py-1 rounded text-xs flex items-center gap-1'>
|
||||
{v.join(' / ')}
|
||||
<button type='button' className='ml-1 text-xs' onClick={() => handleRemove(v)}>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染级联下拉
|
||||
const renderCascader = () => {
|
||||
const currentOptions = getCurrentOptions();
|
||||
return (
|
||||
<div className='absolute z-50 bg-white border rounded shadow p-2 mt-1 min-w-[200px]'>
|
||||
{activePath.length > 0 && (
|
||||
<div className='mb-2 flex gap-1 text-xs text-muted-foreground'>
|
||||
{activePath.map((t, i) => (
|
||||
<span key={i} className='cursor-pointer hover:underline' onClick={() => setActivePath(activePath.slice(0, i + 1))}>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<ul>
|
||||
{currentOptions.map(opt => (
|
||||
<li key={opt.title} className='flex items-center gap-2 py-1'>
|
||||
{opt.children && opt.children.length > 0 ? (
|
||||
<>
|
||||
<span className='cursor-pointer hover:text-primary' onClick={() => setActivePath([...activePath, opt.title])}>
|
||||
{opt.title}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<label className='flex items-center gap-1 cursor-pointer'>
|
||||
<input type='checkbox' checked={isSelected([...activePath, opt.title])} onChange={() => handleSelect([...activePath, opt.title])} />
|
||||
<span>{opt.title}</span>
|
||||
</label>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<div className='flex items-center gap-2 flex-wrap'>
|
||||
{renderTags()}
|
||||
<button
|
||||
type='button'
|
||||
className='border px-2 py-1 rounded text-xs bg-white hover:bg-muted'
|
||||
onClick={() => {
|
||||
setOpen(v => !v);
|
||||
setActivePath([]);
|
||||
}}
|
||||
>
|
||||
选择场景
|
||||
</button>
|
||||
</div>
|
||||
{open && renderCascader()}
|
||||
{open && <div className='fixed inset-0 z-40' onClick={() => setOpen(false)} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CascaderMultiSelect;
|
||||
116
src/pages/TryOnPage/components/ClothingCard.tsx
Normal file
116
src/pages/TryOnPage/components/ClothingCard.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
|
||||
import { Card, CardHeader, CardAction, CardContent } from '@/components/ui/card';
|
||||
import { X } from 'lucide-react';
|
||||
import CascaderMultiSelect from './CascaderMultiSelect';
|
||||
|
||||
interface Tag {
|
||||
sex: string;
|
||||
category: string;
|
||||
size: string;
|
||||
material: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface TagOptions {
|
||||
sexOptions: any[];
|
||||
categoryOptions: any[];
|
||||
sizeOptions: any[];
|
||||
materialOptions: any[];
|
||||
colorOptions: any[];
|
||||
}
|
||||
|
||||
interface ClothingCardProps {
|
||||
imageUrl: string;
|
||||
tag: Tag;
|
||||
options: TagOptions;
|
||||
idx: number;
|
||||
onTagChange: (level: keyof Tag, value: string) => void;
|
||||
onRemove: () => void;
|
||||
errors?: Partial<Record<keyof Tag, string>>;
|
||||
scenesOptions?: any[];
|
||||
scenes?: string[][];
|
||||
onScenesChange?: (scenes: string[][]) => void;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
const LABELS = ['性别', '类别', '尺寸', '材质', '颜色'];
|
||||
const LEVELS: (keyof Tag)[] = ['sex', 'category', 'size', 'material', 'color'];
|
||||
|
||||
const ClothingCard: React.FC<ClothingCardProps> = ({
|
||||
imageUrl,
|
||||
tag,
|
||||
options,
|
||||
onTagChange,
|
||||
onRemove,
|
||||
errors = {},
|
||||
scenesOptions = [],
|
||||
scenes = [],
|
||||
onScenesChange,
|
||||
fileName,
|
||||
}) => {
|
||||
return (
|
||||
<Card className='relative md:flex-row gap-6 group hover:shadow-lg transition-shadow'>
|
||||
<CardHeader className='p-0'>
|
||||
<CardAction>
|
||||
<Button
|
||||
type='button'
|
||||
size='icon'
|
||||
variant='destructive'
|
||||
onClick={onRemove}
|
||||
className='absolute -top-3 -right-2 w-6 h-6 p-0 rounded-full shadow transition-colors transition-opacity opacity-0 group-hover:opacity-100'
|
||||
title='删除'
|
||||
>
|
||||
<X size={18} />
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className='flex items-center w-full gap-6'>
|
||||
<div className='flex flex-col items-center'>
|
||||
<img src={imageUrl} alt='预览' className='w-[200px] aspect-[9/16] object-cover rounded border max-h-72' />
|
||||
{fileName && <div className='mt-2 text-xs text-muted-foreground break-all max-w-[200px]'>{fileName}</div>}
|
||||
</div>
|
||||
<div className='flex-1 flex flex-col gap-6'>
|
||||
{LEVELS.map((level, lidx) => {
|
||||
let selectOptions: any[] = [];
|
||||
if (level === 'sex') selectOptions = options.sexOptions;
|
||||
else if (level === 'category') selectOptions = options.categoryOptions;
|
||||
else if (level === 'size') selectOptions = options.sizeOptions;
|
||||
else if (level === 'material') selectOptions = options.materialOptions;
|
||||
else if (level === 'color') selectOptions = options.colorOptions;
|
||||
return (
|
||||
<div key={level} className='w-full flex flex-row items-center gap-2'>
|
||||
<label className='w-16 text-right flex-shrink-0'>{LABELS[lidx]}</label>
|
||||
<div className='flex-1'>
|
||||
<Select value={tag[level]} onValueChange={v => onTagChange(level, v)} disabled={selectOptions.length === 0}>
|
||||
<SelectTrigger className='w-full'>
|
||||
<SelectValue placeholder={`请选择${LABELS[lidx]}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{selectOptions.map((item: any) => (
|
||||
<SelectItem key={item.title} value={item.title}>
|
||||
{item.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors[level] && <div className='text-destructive text-xs mt-1'>{errors[level]}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* 场景级联多选 */}
|
||||
<div className='w-full flex flex-row items-center gap-2'>
|
||||
<label className='w-16 text-right flex-shrink-0'>场景</label>
|
||||
<div className='flex-1'>
|
||||
<CascaderMultiSelect options={scenesOptions} value={scenes || []} onChange={onScenesChange || (() => {})} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClothingCard;
|
||||
241
src/pages/TryOnPage/index.tsx
Normal file
241
src/pages/TryOnPage/index.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import type { Body_local_async_gen_images_api_v2_local_batch_edit_images_post } from '@/api/models/Body_local_async_gen_images_api_v2_local_batch_edit_images_post';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { api } from '@/lib/api';
|
||||
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';
|
||||
|
||||
// 五级联动的 tag schema
|
||||
const tagSchema = 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, '请选择颜色'),
|
||||
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 {
|
||||
sex: string;
|
||||
category: string;
|
||||
size: string;
|
||||
material: string;
|
||||
color: string;
|
||||
scenes?: string[][];
|
||||
}
|
||||
|
||||
interface FormValues {
|
||||
clothing_images: File[];
|
||||
tags: TagForm[];
|
||||
changeBg?: boolean;
|
||||
}
|
||||
|
||||
const defaultValues: FormValues = {
|
||||
clothing_images: [],
|
||||
tags: [],
|
||||
changeBg: false,
|
||||
};
|
||||
|
||||
const TryOnPage: React.FC = () => {
|
||||
const [tagOptions, setTagOptions] = useState<any[]>([]);
|
||||
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.Service.fetchModelTagsApiTagModelTagListGet().then(data => setTagOptions(data.data || []));
|
||||
api.Service.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(() => ({ sex: '', category: '', size: '', material: '', color: '', 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 handleCascade = (idx: number, level: keyof TagForm, value: string) => {
|
||||
const newTags = [...form.getValues('tags')];
|
||||
if (level === 'sex') {
|
||||
newTags[idx] = { sex: value, category: '', size: '', material: '', color: '', scenes: newTags[idx]?.scenes || [] };
|
||||
} else if (level === 'category') {
|
||||
newTags[idx] = { ...newTags[idx], category: value, size: '', material: '', color: '', scenes: newTags[idx]?.scenes || [] };
|
||||
} else if (level === 'size') {
|
||||
newTags[idx] = { ...newTags[idx], size: value, material: '', color: '', scenes: newTags[idx]?.scenes || [] };
|
||||
} else if (level === 'material') {
|
||||
newTags[idx] = { ...newTags[idx], material: value, scenes: newTags[idx]?.scenes || [] };
|
||||
} else if (level === 'color') {
|
||||
newTags[idx] = { ...newTags[idx], color: value, scenes: newTags[idx]?.scenes || [] };
|
||||
}
|
||||
form.setValue('tags', newTags);
|
||||
};
|
||||
|
||||
// 场景多选
|
||||
const handleScenesChange = (idx: number, scenes: string[][]) => {
|
||||
const newTags = [...form.getValues('tags')];
|
||||
newTags[idx] = { ...newTags[idx], scenes };
|
||||
form.setValue('tags', newTags);
|
||||
};
|
||||
|
||||
// 计算每个卡片的options
|
||||
const getTagOptions = (idx: number) => {
|
||||
const tag = tags[idx] || {};
|
||||
// 性别
|
||||
const sexOptions = tagOptions;
|
||||
// 类别
|
||||
const categoryOptions = tag.sex ? tagOptions.find((t: any) => t.title === tag.sex)?.children || [] : [];
|
||||
// 尺寸
|
||||
const sizeOptions = tag.category ? categoryOptions.find((t: any) => t.title === tag.category)?.children || [] : [];
|
||||
// 材质
|
||||
const materialOptions = tag.size
|
||||
? sizeOptions.find((t: any) => t.title === tag.size)?.children?.find((c: any) => c.title === '材质')?.children || []
|
||||
: [];
|
||||
// 颜色
|
||||
const colorOptions = tag.size
|
||||
? sizeOptions.find((t: any) => t.title === tag.size)?.children?.find((c: any) => c.title === '颜色')?.children || []
|
||||
: [];
|
||||
return { sexOptions, categoryOptions, sizeOptions, materialOptions, colorOptions };
|
||||
};
|
||||
|
||||
// 递归查找 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[] = [];
|
||||
try {
|
||||
for (let i = 0; i < values.clothing_images.length; i++) {
|
||||
const tag = values.tags[i];
|
||||
const scenesArr: string[][] = tag.scenes || [];
|
||||
const scenes_list = scenesArr.map((path: string[]) => ({
|
||||
title: path.join('/'),
|
||||
prompt: findPromptByPath(scenesOptions, path),
|
||||
}));
|
||||
const formData: Body_local_async_gen_images_api_v2_local_batch_edit_images_post = {
|
||||
clothing_images: [values.clothing_images[i]],
|
||||
tag_list: JSON.stringify([[tag.sex, tag.category, tag.size, tag.material, tag.color].join('_')]),
|
||||
scenes_list: JSON.stringify(scenes_list.map(s => ({ title: s.title, prompt: s.prompt }))),
|
||||
mode: 'both',
|
||||
};
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const res = await api.DefaultService.localAsyncGenImagesApiV2LocalBatchEditImagesPost({ 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] || { sex: '', category: '', size: '', material: '', color: '', scenes: [] };
|
||||
const { sexOptions, categoryOptions, sizeOptions, materialOptions, colorOptions } = getTagOptions(idx);
|
||||
const fileName = clothing_images[idx]?.name;
|
||||
return (
|
||||
<ClothingCard
|
||||
key={idx}
|
||||
imageUrl={imagePreviews[idx]}
|
||||
tag={tag}
|
||||
options={{ sexOptions, categoryOptions, sizeOptions, materialOptions, colorOptions }}
|
||||
idx={idx}
|
||||
onTagChange={(level, value) => handleCascade(idx, level, value)}
|
||||
onRemove={() => handleRemoveImage(idx)}
|
||||
scenesOptions={scenesOptions}
|
||||
scenes={tag.scenes || []}
|
||||
onScenesChange={scenes => handleScenesChange(idx, scenes)}
|
||||
fileName={fileName}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<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;
|
||||
Reference in New Issue
Block a user