diff --git a/frontend/src/pages/quick-start/index.tsx b/frontend/src/pages/quick-start/index.tsx index de032d69..be476fc0 100644 --- a/frontend/src/pages/quick-start/index.tsx +++ b/frontend/src/pages/quick-start/index.tsx @@ -32,6 +32,7 @@ import Markdown, { compiler } from 'markdown-to-jsx' import { Link, useLocation, useNavigate, useParams, useSearchParams } from 'react-router' import { InlineArrowAction } from './inline-arrow-action' import { PixelPerfectVersionSwitch, type PixelPerfectVersion } from './pixel-perfect-version-switch' +import { Render3DOption } from './render3d-option' import { ART_STYLE, @@ -3342,6 +3343,7 @@ function QuickStartRun({ className="aspect-square w-full rounded-2xl border border-app-line bg-app-surface-muted object-contain [image-rendering:pixelated]" /> + > ) : workflowHasFailure(revision) ? ( <> diff --git a/frontend/src/pages/quick-start/render3d-option.test.tsx b/frontend/src/pages/quick-start/render3d-option.test.tsx new file mode 100644 index 00000000..5b9d438e --- /dev/null +++ b/frontend/src/pages/quick-start/render3d-option.test.tsx @@ -0,0 +1,65 @@ +/** @vitest-environment jsdom */ +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/entities', async () => { + const actual = await vi.importActual>('@/entities') + return { + ...actual, + render3DApis: { + precheckMaster: async () => ({}), + getOutfitAsset: async () => ({ + state: 'absent', + model3dUrl: null, + reviewModelUrl: null, + error: null, + cost: { + model3dCredits: 20, + autorigCredits: 10, + totalCredits: 30, + billing: 'postpaid', + scope: 'per_outfit_once', + }, + }), + buildOutfitAsset: async () => ({}), + approveOutfitAsset: async () => ({}), + discardOutfitAsset: async () => ({}), + getBakeJob: async () => null, + putBakeFrame: async () => 1, + completeBake: async () => undefined, + failBake: async () => undefined, + }, + } +}) + +const { Render3DOption } = await import('./render3d-option') + +afterEach(cleanup) + +describe('Quick Start 的 3D 进阶入口', () => { + it('默认折叠 —— 主线是「一句话跑完」,按次计费的重动作不该摊在主线上', () => { + render( + ({ characterId: '7', outfitId: 'outfit-default' }) }} + />, + ) + expect(screen.getByRole('button', { name: /建 3D 资产/ })).toBeTruthy() + expect(screen.queryByRole('group', { name: '3D 资产' })).toBeNull() + }) + + it('展开后才出现资产面板', async () => { + render( + ({ characterId: '7', outfitId: 'outfit-default' }) }} + />, + ) + fireEvent.click(screen.getByRole('button', { name: /建 3D 资产/ })) + expect(await screen.findByRole('group', { name: '3D 资产' })).toBeTruthy() + expect(screen.getByText(/只支持双足人形/)).toBeTruthy() + }) + + it('还没有角色时不渲染 —— 没有 outfit 就没有可建的对象', () => { + const { container } = render( null }} />) + expect(container.textContent).toBe('') + }) +}) diff --git a/frontend/src/pages/quick-start/render3d-option.tsx b/frontend/src/pages/quick-start/render3d-option.tsx new file mode 100644 index 00000000..2f8392a8 --- /dev/null +++ b/frontend/src/pages/quick-start/render3d-option.tsx @@ -0,0 +1,59 @@ +/** + * 角色确认之后的可选进阶路线:给这个造型建 3D 资产(Refs #518)。 + * + * 默认折叠。Quick Start 的主线是「一句话跑完」,而建 3D 是按次计费、带人工确认闸、 + * 每账号还有名额上限的重动作 —— 摊开在主线上会把它读成必经的一步。 + */ +import { useState } from 'react' + +import { render3DApis } from '@/entities' +import { Render3DAssetPanel } from '@/pages/workflow-editor/render3d-panel' + +/** 只取建资产要的那两个 id;不依赖整个会话,测试也就不必造一份完整会话。 */ +export interface Render3DOptionSession { + getCharacterInfo(): { characterId: string; outfitId: string } | null +} + +export function Render3DOption({ session }: { session: Render3DOptionSession | null }) { + const [open, setOpen] = useState(false) + const info = session?.getCharacterInfo() ?? null + if (!info) return null + + return ( + + {open ? ( + + + 3D 资产 · 三渲二 + setOpen(false)} + > + 收起 + + + + 建好后这个角色的每个动作都能改用三渲二出帧:多朝向零额外费用、各朝向天生一致。 + 目前只支持双足人形。 + + + + ) : ( + setOpen(true)} + > + 进阶 · 给这个角色建 3D 资产 + + )} + + ) +}
3D 资产 · 三渲二
+ 建好后这个角色的每个动作都能改用三渲二出帧:多朝向零额外费用、各朝向天生一致。 + 目前只支持双足人形。 +