feat: 完善

This commit is contained in:
2025-06-27 17:12:28 +08:00
parent 8936b898e2
commit be289b72b3
25 changed files with 655 additions and 397 deletions

View File

@@ -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>

View File

@@ -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';

View File

@@ -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>;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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>;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
},
},

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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: {

View File

@@ -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`,
},
});
}
}

View File

@@ -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',
});
}
}

View 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,
}

View 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;

View 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;

View 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;