diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index b21413bde..8841b9ae0 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -467,6 +467,10 @@ export interface AdminEditorAssetPayload { generationCostMudPoints: number; createdAt: string; updatedAt: string; + generator: string; + taskGenerator: string; + taskCostMudPoints: number; + children: AdminEditorAssetPayload[]; } export interface AdminEditorAssetListResponse { diff --git a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx index ddbc7a502..9d360b81c 100644 --- a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx +++ b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx @@ -148,6 +148,10 @@ const generatedAsset: AdminEditorAssetPayload = { generationCostMudPoints: 12, createdAt: '2026-07-04T10:00:00Z', updatedAt: '2026-07-04T10:00:00Z', + generator: '角色抠图', + taskGenerator: '角色生成', + taskCostMudPoints: 12, + children: [], }; beforeEach(() => { @@ -245,17 +249,45 @@ test('后台素材查询不展示分类筛选和分类列', async () => { expect(screen.queryByRole('columnheader', { name: '分类' })).toBeNull(); }); -test('后台素材查询将中间产物标记为不单独计费', async () => { +test('后台素材查询将中间产物按真实类型逐行收纳在最终产物下', async () => { + const providerSource: AdminEditorAssetPayload = { + ...generatedAsset, + assetId: 'asset-source', + label: '角色形象生成原图', + objectKey: + 'generated-character-drafts/editor/character-images/provider-source-task-1.png', + imageSrc: + '/generated-character-drafts/editor/character-images/provider-source-task-1.png', + assetKind: 'character', + generator: '角色生图', + taskGenerator: '角色生成', + generationCostMudPoints: 12, + taskCostMudPoints: 12, + children: [], + }; + const postprocessOutput: AdminEditorAssetPayload = { + ...generatedAsset, + assetId: 'asset-postprocess', + label: '角色抠图阶段产物', + objectKey: + 'generated-character-drafts/editor/character-images/background-removal-task-1.png', + imageSrc: + '/generated-character-drafts/editor/character-images/background-removal-task-1.png', + assetKind: 'character', + generator: '角色抠图', + taskGenerator: '角色生成', + generationCostMudPoints: 0, + taskCostMudPoints: 0, + children: [], + }; vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({ entries: [ { ...generatedAsset, - assetId: 'asset-source', - label: '角色形象原图', - assetKind: 'editor_green_screen_source', generationCostMudPoints: 0, + taskCostMudPoints: 12, + children: [providerSource, postprocessOutput], }, - generatedAsset, ], nextCursor: null, }); @@ -264,19 +296,31 @@ test('后台素材查询将中间产物标记为不单独计费', async () => { , ); - const sourceRow = (await screen.findByText('角色形象原图')).closest('tr'); - const finalRow = screen.getByText('角色形象 1').closest('tr'); - expect(sourceRow).not.toBeNull(); + const finalRow = (await screen.findByText('角色形象 1')).closest('tr'); expect(finalRow).not.toBeNull(); - expect(within(sourceRow!).getByText('不单独计费')).toBeTruthy(); expect(within(finalRow!).getByText('12 泥点')).toBeTruthy(); + expect(screen.queryByText('角色形象生成原图')).toBeNull(); + expect(screen.queryByText('角色抠图阶段产物')).toBeNull(); + + fireEvent.click( + within(finalRow!).getByRole('button', { name: '展开中间产物' }), + ); + + const sourceRow = (await screen.findByText('角色形象生成原图')).closest('tr'); + const postprocessRow = screen.getByText('角色抠图阶段产物').closest('tr'); + expect(sourceRow).not.toBeNull(); + expect(postprocessRow).not.toBeNull(); + expect(within(sourceRow!).getByText('角色生图')).toBeTruthy(); + expect(within(sourceRow!).getByText('12 泥点')).toBeTruthy(); + expect(within(postprocessRow!).getByText('角色抠图')).toBeTruthy(); + expect( + within(postprocessRow!).getByText('本阶段不额外扣费'), + ).toBeTruthy(); fireEvent.click(within(sourceRow!).getByRole('button', { name: '详情' })); - expect( - within(await screen.findByRole('dialog', { name: '素材详情' })).getByText( - '不单独计费', - ), - ).toBeTruthy(); + const detail = await screen.findByRole('dialog', { name: '素材详情' }); + expect(within(detail).getByText('角色生图')).toBeTruthy(); + expect(within(detail).getByText('12 泥点')).toBeTruthy(); }); test('后台素材查询缩略图使用 objectKey 换签后展示', async () => { diff --git a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx index d8276fe8f..e23a00aae 100644 --- a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx +++ b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx @@ -1,5 +1,5 @@ -import { Eye, FileText, RefreshCcw, X } from 'lucide-react'; -import type { ReactNode } from 'react'; +import { ChevronDown, ChevronRight, Eye, FileText, RefreshCcw, X } from 'lucide-react'; +import { Fragment, type ReactNode } from 'react'; import { useCallback, useEffect, useState } from 'react'; import { @@ -24,10 +24,6 @@ const ADMIN_ASSET_READ_EXPIRE_SECONDS = 300; const ADMIN_ASSET_READ_DISPATCH_SPACING_MS = 40; const ADMIN_ASSET_READ_RETRY_DELAYS_MS = [400, 1_200, 3_000] as const; const ADMIN_ASSET_THUMBNAIL_ROOT_MARGIN = '240px 0px'; -const NON_BILLED_INTERMEDIATE_ASSET_KINDS = new Set([ - 'editor_green_screen_source', - 'editor_image_edit_source', -]); const AUDIO_ASSET_COVER_SRC = `${import.meta.env.DEV ? import.meta.env.BASE_URL : '/'}creation-home/audio-asset-cover.png`; let adminAssetReadDispatchTail = Promise.resolve(); @@ -48,6 +44,9 @@ export function AdminEditorAssetQueryPage({ useState(null); const [previewEntry, setPreviewEntry] = useState(null); + const [expandedTaskIds, setExpandedTaskIds] = useState>( + () => new Set(), + ); const [promptPreview, setPromptPreview] = useState<{ title: string; prompt: string; @@ -153,7 +152,7 @@ export function AdminEditorAssetQueryPage({