接入创作主页画布直达引导
创作工具卡片接入画布生成器直达意图 新建空白画布增加工具栏引导并在生成面板打开后关闭 暂未开放工具复用移动端欢迎弹窗提示 补充主页、项目入口和画布启动意图测试 同步创作主页入口改版文档
This commit is contained in:
@@ -195,10 +195,80 @@ describe('CreationLandingView', () => {
|
||||
await user.click(screen.getByRole('button', { name: /开始创作/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onOpenProject).toHaveBeenCalledWith('created-project');
|
||||
expect(onOpenProject).toHaveBeenCalledWith('created-project', {
|
||||
guide: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['游戏视觉规范', 'icon-spec'],
|
||||
['游戏角色', 'character-spec'],
|
||||
['游戏UI', 'ui-design'],
|
||||
['游戏音乐', 'background-music'],
|
||||
['游戏场景', 'image'],
|
||||
['游戏美宣', 'publication-cover'],
|
||||
])(
|
||||
'creates a new project from %s with its canvas tool intent',
|
||||
async (label, expectedTool) => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenProject = vi.fn();
|
||||
listEditorProjectsMock.mockResolvedValueOnce([]);
|
||||
createEditorProjectMock.mockResolvedValueOnce({
|
||||
projectId: 'visual-spec-project',
|
||||
title: '未命名画布',
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
layers: [],
|
||||
resources: [],
|
||||
updatedAt: '2026-06-18T10:00:00.000Z',
|
||||
});
|
||||
|
||||
render(
|
||||
<AuthUiContext.Provider value={createAuthValue()}>
|
||||
<CreationLandingView
|
||||
onOpenProject={onOpenProject}
|
||||
onOpenProjects={vi.fn()}
|
||||
/>
|
||||
</AuthUiContext.Provider>,
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: new RegExp(label, 'u') }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onOpenProject).toHaveBeenCalledWith('visual-spec-project', {
|
||||
tool: expectedTool,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('shows the pending feature dialog for unavailable creation tool cards', async () => {
|
||||
const user = userEvent.setup();
|
||||
listEditorProjectsMock.mockResolvedValueOnce([]);
|
||||
|
||||
render(
|
||||
<AuthUiContext.Provider value={createAuthValue()}>
|
||||
<CreationLandingView
|
||||
onOpenProject={vi.fn()}
|
||||
onOpenProjects={vi.fn()}
|
||||
/>
|
||||
</AuthUiContext.Provider>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /游戏特效/u }));
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '抱歉' });
|
||||
expect(dialog.textContent).toContain('功能还在调试中');
|
||||
expect(dialog.textContent).toContain('暂未开放');
|
||||
expect(createEditorProjectMock).not.toHaveBeenCalled();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '期待一下' }));
|
||||
|
||||
expect(screen.queryByRole('dialog', { name: '抱歉' })).toBeNull();
|
||||
});
|
||||
|
||||
it('opens the community entry from the hero without starting project creation', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenCommunity = vi.fn();
|
||||
|
||||
@@ -29,15 +29,27 @@ import {
|
||||
} from './creationShowcaseModel';
|
||||
|
||||
type CreationLandingViewProps = {
|
||||
onOpenProject: (projectId: string) => void;
|
||||
onOpenProject: (
|
||||
projectId: string,
|
||||
options?: { guide?: boolean; tool?: CreationFeatureTool },
|
||||
) => void;
|
||||
onOpenProjects: () => void;
|
||||
onOpenCommunity?: () => void;
|
||||
};
|
||||
|
||||
type CreationFeatureTool =
|
||||
| 'character-spec'
|
||||
| 'icon-spec'
|
||||
| 'ui-design'
|
||||
| 'background-music'
|
||||
| 'image'
|
||||
| 'publication-cover';
|
||||
|
||||
type CreationFeatureItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
iconSrc: string;
|
||||
action: { kind: 'tool'; tool: CreationFeatureTool } | { kind: 'pending' };
|
||||
};
|
||||
|
||||
const CREATION_HOME_ASSETS = {
|
||||
@@ -51,46 +63,55 @@ const CREATION_FEATURES: CreationFeatureItem[] = [
|
||||
title: '游戏视觉规范',
|
||||
description: '轻松约束多类素材视觉一致性',
|
||||
iconSrc: '/creation-home/feature-visual-spec.png',
|
||||
action: { kind: 'tool', tool: 'icon-spec' },
|
||||
},
|
||||
{
|
||||
title: '游戏角色',
|
||||
description: '整套高完成度2D/3D角色素材及动画',
|
||||
iconSrc: '/creation-home/feature-character.png',
|
||||
action: { kind: 'tool', tool: 'character-spec' },
|
||||
},
|
||||
{
|
||||
title: '游戏UI',
|
||||
description: '轻松生成高一致性的UI素材',
|
||||
iconSrc: '/creation-home/feature-ui.png',
|
||||
action: { kind: 'tool', tool: 'ui-design' },
|
||||
},
|
||||
{
|
||||
title: '游戏音乐',
|
||||
description: '轻松制作游戏背景音乐、音效',
|
||||
iconSrc: '/creation-home/feature-music.png',
|
||||
action: { kind: 'tool', tool: 'background-music' },
|
||||
},
|
||||
{
|
||||
title: '游戏特效',
|
||||
description: '低成本生成精品游戏特效',
|
||||
iconSrc: '/creation-home/feature-effects.png',
|
||||
action: { kind: 'pending' },
|
||||
},
|
||||
{
|
||||
title: '游戏场景',
|
||||
description: '轻松生成各类游戏场景素材',
|
||||
iconSrc: '/creation-home/feature-scene.png',
|
||||
action: { kind: 'tool', tool: 'image' },
|
||||
},
|
||||
{
|
||||
title: '游戏美宣',
|
||||
description: '轻松生成游戏上架所需图片和视频',
|
||||
iconSrc: '/creation-home/feature-marketing.png',
|
||||
action: { kind: 'tool', tool: 'publication-cover' },
|
||||
},
|
||||
{
|
||||
title: '游戏功能编码',
|
||||
description: '自然语言驱动游戏生成',
|
||||
iconSrc: '/creation-home/feature-coding.png',
|
||||
action: { kind: 'pending' },
|
||||
},
|
||||
{
|
||||
title: '一键导出',
|
||||
description: '支持导出为微抖小游戏、Godot、Unity、UE工程',
|
||||
iconSrc: '/creation-home/feature-export.png',
|
||||
action: { kind: 'pending' },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -377,6 +398,8 @@ export function CreationLandingView({
|
||||
const [selectedShowcaseItem, setSelectedShowcaseItem] =
|
||||
useState<ShowcaseAssetItem | null>(null);
|
||||
const [selectedShowcaseIndex, setSelectedShowcaseIndex] = useState(0);
|
||||
const [isPendingFeatureDialogOpen, setIsPendingFeatureDialogOpen] =
|
||||
useState(false);
|
||||
|
||||
const refreshRecentProjects = useCallback(() => {
|
||||
if (!isAuthenticated) {
|
||||
@@ -431,18 +454,20 @@ export function CreationLandingView({
|
||||
refreshShowcaseResources();
|
||||
}, [refreshShowcaseResources]);
|
||||
|
||||
const createProject = useCallback(() => {
|
||||
const createProject = useCallback((tool?: CreationFeatureTool) => {
|
||||
if (authUi && !authUi.user) {
|
||||
authUi.openLoginModal(createProject);
|
||||
authUi.openLoginModal(() => createProject(tool));
|
||||
return;
|
||||
}
|
||||
setIsCreatingProject(true);
|
||||
setProjectError(null);
|
||||
createEditorProject()
|
||||
.then((project) => onOpenProject(project.projectId))
|
||||
.then((project) =>
|
||||
onOpenProject(project.projectId, tool ? { tool } : { guide: true }),
|
||||
)
|
||||
.catch((error: unknown) => {
|
||||
if (isUnauthorizedError(error)) {
|
||||
authUi?.openLoginModal(createProject);
|
||||
authUi?.openLoginModal(() => createProject(tool));
|
||||
return;
|
||||
}
|
||||
setProjectError(
|
||||
@@ -452,6 +477,17 @@ export function CreationLandingView({
|
||||
.finally(() => setIsCreatingProject(false));
|
||||
}, [authUi, onOpenProject]);
|
||||
|
||||
const handleFeatureClick = useCallback(
|
||||
(feature: CreationFeatureItem) => {
|
||||
if (feature.action.kind === 'pending') {
|
||||
setIsPendingFeatureDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
createProject(feature.action.tool);
|
||||
},
|
||||
[createProject],
|
||||
);
|
||||
|
||||
const showcaseItems = useMemo(
|
||||
() =>
|
||||
buildCreationShowcaseItems({
|
||||
@@ -533,7 +569,7 @@ export function CreationLandingView({
|
||||
size="md"
|
||||
shape="pill"
|
||||
className="creation-landing__hero-action creation-landing__hero-action--primary"
|
||||
onClick={createProject}
|
||||
onClick={() => createProject()}
|
||||
disabled={isCreatingProject}
|
||||
>
|
||||
<img
|
||||
@@ -580,7 +616,13 @@ export function CreationLandingView({
|
||||
</div>
|
||||
<div className="creation-landing__feature-grid">
|
||||
{CREATION_FEATURES.map((feature) => (
|
||||
<article key={feature.title} className="creation-landing__feature">
|
||||
<button
|
||||
key={feature.title}
|
||||
type="button"
|
||||
className="creation-landing__feature"
|
||||
onClick={() => handleFeatureClick(feature)}
|
||||
disabled={isCreatingProject && feature.action.kind === 'tool'}
|
||||
>
|
||||
<span className="creation-landing__feature-figure">
|
||||
<span className="creation-landing__feature-icon">
|
||||
<img
|
||||
@@ -597,7 +639,7 @@ export function CreationLandingView({
|
||||
<h3>{feature.title}</h3>
|
||||
<p>{feature.description}</p>
|
||||
</span>
|
||||
</article>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
@@ -625,7 +667,7 @@ export function CreationLandingView({
|
||||
<button
|
||||
type="button"
|
||||
className="creation-landing__project-card creation-landing__project-card--new"
|
||||
onClick={createProject}
|
||||
onClick={() => createProject()}
|
||||
disabled={isCreatingProject}
|
||||
>
|
||||
<span>
|
||||
@@ -713,6 +755,41 @@ export function CreationLandingView({
|
||||
onSelectIndex={setSelectedShowcaseIndex}
|
||||
onClose={() => setSelectedShowcaseItem(null)}
|
||||
/>
|
||||
<UnifiedModal
|
||||
open={isPendingFeatureDialogOpen}
|
||||
title="抱歉"
|
||||
onClose={() => setIsPendingFeatureDialogOpen(false)}
|
||||
showHeader={false}
|
||||
showCloseButton={false}
|
||||
size="sm"
|
||||
overlayClassName="platform-mobile-home-welcome-overlay !items-center !p-4"
|
||||
panelClassName="platform-remap-surface platform-mobile-home-welcome-dialog"
|
||||
bodyClassName="platform-mobile-home-welcome-dialog__body"
|
||||
footerClassName="platform-mobile-home-welcome-dialog__footer"
|
||||
footer={
|
||||
<PlatformActionButton
|
||||
onClick={() => setIsPendingFeatureDialogOpen(false)}
|
||||
size="md"
|
||||
shape="pill"
|
||||
fullWidth
|
||||
className="min-h-11"
|
||||
>
|
||||
期待一下
|
||||
</PlatformActionButton>
|
||||
}
|
||||
>
|
||||
<img
|
||||
src="/branding/mobile-home-welcome-taonier-ip.png"
|
||||
alt=""
|
||||
className="platform-mobile-home-welcome-dialog__icon"
|
||||
/>
|
||||
<h1 className="platform-mobile-home-welcome-dialog__title">抱歉</h1>
|
||||
<p className="platform-mobile-home-welcome-dialog__copy">
|
||||
功能还在调试中
|
||||
<br />
|
||||
暂未开放
|
||||
</p>
|
||||
</UnifiedModal>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ type ImageCanvasBottomToolbarViewProps = {
|
||||
musicToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
publicationToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
effectiveTool: CanvasTool;
|
||||
highlight?: boolean;
|
||||
onSwitchTool: (tool: CanvasTool) => void;
|
||||
onOpenToolOptions: (tool: ToolbarOptionTool) => void;
|
||||
onCloseToolOptions: (tool: ToolbarOptionTool) => void;
|
||||
@@ -70,13 +71,19 @@ export function ImageCanvasBottomToolbarView({
|
||||
musicToolWrapRef,
|
||||
publicationToolWrapRef,
|
||||
effectiveTool,
|
||||
highlight = false,
|
||||
onSwitchTool,
|
||||
onOpenToolOptions,
|
||||
onCloseToolOptions,
|
||||
}: ImageCanvasBottomToolbarViewProps) {
|
||||
return (
|
||||
<div
|
||||
className="image-canvas-editor__bottom-toolbar"
|
||||
className={[
|
||||
'image-canvas-editor__bottom-toolbar',
|
||||
highlight ? 'image-canvas-editor__bottom-toolbar--guided' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
role="toolbar"
|
||||
aria-label="AI画布工具栏"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
|
||||
@@ -127,6 +127,75 @@ describe('ImageCanvasEditorView', () => {
|
||||
expect(loadOrCreateRecentEditorProjectMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows the toolbar guide for a newly created blank project until a generator opens', async () => {
|
||||
loadEditorProjectMock.mockResolvedValueOnce({
|
||||
projectId: 'editor-project-guide',
|
||||
title: '空白项目',
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
layers: [],
|
||||
resources: [],
|
||||
updatedAt: '2026-06-12T00:00:00.000Z',
|
||||
});
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/editor/canvas?projectid=editor-project-guide&guide=toolbar',
|
||||
);
|
||||
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
expect(
|
||||
await screen.findByText('工具栏在这里,快试着随便做点什么'),
|
||||
).toBeTruthy();
|
||||
const bottomToolbar = screen.getByRole('toolbar', {
|
||||
name: 'AI画布工具栏',
|
||||
});
|
||||
expect(bottomToolbar.className).toContain(
|
||||
'image-canvas-editor__bottom-toolbar--guided',
|
||||
);
|
||||
expect(window.location.search).toBe('?projectid=editor-project-guide');
|
||||
|
||||
fireEvent.click(
|
||||
within(bottomToolbar).getByRole('button', { name: '生成图片' }),
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText('工具栏在这里,快试着随便做点什么'),
|
||||
).toBeNull();
|
||||
});
|
||||
expect(bottomToolbar.className).not.toContain(
|
||||
'image-canvas-editor__bottom-toolbar--guided',
|
||||
);
|
||||
});
|
||||
|
||||
it('opens the requested canvas startup tool from the project query once', async () => {
|
||||
loadEditorProjectMock.mockResolvedValueOnce({
|
||||
projectId: 'editor-project-music',
|
||||
title: '音乐项目',
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
layers: [],
|
||||
resources: [],
|
||||
updatedAt: '2026-06-12T00:00:00.000Z',
|
||||
});
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/editor/canvas?projectid=editor-project-music&tool=background-music',
|
||||
);
|
||||
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '生成游戏背景音乐' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByText('工具栏在这里,快试着随便做点什么'),
|
||||
).toBeNull();
|
||||
expect(window.location.search).toBe('?projectid=editor-project-music');
|
||||
});
|
||||
|
||||
it('shows the loaded project title and a topbar entry back to projects', async () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
|
||||
@@ -51,7 +51,10 @@ import type {
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { getEditorUploadAccept, isImageFile } from './ImageCanvasFileModel';
|
||||
import { createLayerGenerationDialogDraft } from './ImageCanvasGenerationDialogModel';
|
||||
import { applyEditorGenerationPricingConfig } from './ImageCanvasGenerationModel';
|
||||
import {
|
||||
applyEditorGenerationPricingConfig,
|
||||
isCanvasGenerationDialog,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import {
|
||||
isCanvasGenerationComposerVisible,
|
||||
resolveQuickEditFocusViewport,
|
||||
@@ -90,6 +93,81 @@ const TASK_FOCUS_BOTTOM_INSET = 90;
|
||||
const TASK_FOCUS_DIALOG_BOTTOM_INSET = 280;
|
||||
const TASK_FOCUS_PADDING = 18;
|
||||
|
||||
type CanvasStartupTool =
|
||||
| 'character-spec'
|
||||
| 'icon-spec'
|
||||
| 'ui-design'
|
||||
| 'background-music'
|
||||
| 'image'
|
||||
| 'publication-cover';
|
||||
|
||||
const CANVAS_STARTUP_TOOLS: CanvasStartupTool[] = [
|
||||
'character-spec',
|
||||
'icon-spec',
|
||||
'ui-design',
|
||||
'background-music',
|
||||
'image',
|
||||
'publication-cover',
|
||||
];
|
||||
|
||||
function isCanvasStartupTool(value: string | null): value is CanvasStartupTool {
|
||||
return CANVAS_STARTUP_TOOLS.includes(value as CanvasStartupTool);
|
||||
}
|
||||
|
||||
function readCanvasStartupIntent() {
|
||||
if (typeof window === 'undefined') {
|
||||
return { showToolbarGuide: false, tool: null };
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const rawTool = params.get('tool');
|
||||
return {
|
||||
showToolbarGuide: params.get('guide') === 'toolbar',
|
||||
tool: isCanvasStartupTool(rawTool) ? rawTool : null,
|
||||
};
|
||||
}
|
||||
|
||||
function clearCanvasStartupIntent() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.delete('tool');
|
||||
params.delete('guide');
|
||||
const search = params.toString();
|
||||
window.history.replaceState(
|
||||
window.history.state,
|
||||
'',
|
||||
`${window.location.pathname}${search ? `?${search}` : ''}${window.location.hash}`,
|
||||
);
|
||||
}
|
||||
|
||||
function openCanvasStartupTool(
|
||||
tool: CanvasStartupTool,
|
||||
generationSurface: ReturnType<typeof useImageCanvasGenerationSurface>,
|
||||
) {
|
||||
if (tool === 'character-spec') {
|
||||
generationSurface.openSpecDialog('character');
|
||||
return;
|
||||
}
|
||||
if (tool === 'icon-spec') {
|
||||
generationSurface.openSpecDialog('ui');
|
||||
return;
|
||||
}
|
||||
if (tool === 'ui-design') {
|
||||
generationSurface.openUiDesignGenerationDialog();
|
||||
return;
|
||||
}
|
||||
if (tool === 'background-music') {
|
||||
generationSurface.openBackgroundMusicGenerationDialog();
|
||||
return;
|
||||
}
|
||||
if (tool === 'publication-cover') {
|
||||
generationSurface.openPublicationGenerationDialog('publication-cover-image');
|
||||
return;
|
||||
}
|
||||
generationSurface.openGenerateDialog();
|
||||
}
|
||||
|
||||
function resolveLayerProjectResourceImageSrc(layer: CanvasLayer) {
|
||||
const objectKey = layer.objectKey?.trim();
|
||||
if (objectKey) {
|
||||
@@ -227,8 +305,10 @@ export function ImageCanvasEditorView() {
|
||||
const generationReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const publicationReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const iconSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const startupIntentConsumedRef = useRef(false);
|
||||
const selectedLayerIdRef = useRef<string | null>(null);
|
||||
const showRechargeEntry = shouldShowRechargeEntry();
|
||||
const [isToolbarGuideVisible, setIsToolbarGuideVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
@@ -900,8 +980,12 @@ export function ImageCanvasEditorView() {
|
||||
setViewport,
|
||||
],
|
||||
);
|
||||
const { projectId, appendCanvasLayersWithResources, applyProjectSnapshot } =
|
||||
useImageCanvasProjectPersistence({
|
||||
const {
|
||||
projectId,
|
||||
isProjectReady,
|
||||
appendCanvasLayersWithResources,
|
||||
applyProjectSnapshot,
|
||||
} = useImageCanvasProjectPersistence({
|
||||
refs: projectPersistenceRefs,
|
||||
setters: projectPersistenceSetters,
|
||||
layers,
|
||||
@@ -1110,6 +1194,35 @@ export function ImageCanvasEditorView() {
|
||||
setQuickEditPanelRef.current = setQuickEditPanel;
|
||||
appendUiAssetExtractionReferencesRef.current =
|
||||
appendUiAssetExtractionReferences;
|
||||
useEffect(() => {
|
||||
if (!isProjectReady || startupIntentConsumedRef.current) {
|
||||
return;
|
||||
}
|
||||
const startupIntent = readCanvasStartupIntent();
|
||||
if (!startupIntent.showToolbarGuide && !startupIntent.tool) {
|
||||
startupIntentConsumedRef.current = true;
|
||||
return;
|
||||
}
|
||||
startupIntentConsumedRef.current = true;
|
||||
clearCanvasStartupIntent();
|
||||
if (startupIntent.tool) {
|
||||
setIsToolbarGuideVisible(false);
|
||||
openCanvasStartupTool(startupIntent.tool, generationSurface);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
startupIntent.showToolbarGuide &&
|
||||
layersRef.current.length === 0 &&
|
||||
canvasGenerationDialogsRef.current.length === 0
|
||||
) {
|
||||
setIsToolbarGuideVisible(true);
|
||||
}
|
||||
}, [generationSurface, isProjectReady]);
|
||||
useEffect(() => {
|
||||
if (isToolbarGuideVisible && isCanvasGenerationDialog(generateDialog)) {
|
||||
setIsToolbarGuideVisible(false);
|
||||
}
|
||||
}, [generateDialog, isToolbarGuideVisible]);
|
||||
const clearGenerationPanelsAfterBlur = useCallback(() => {
|
||||
setQuickEditPanel(null);
|
||||
setCropExpandPanel(null);
|
||||
@@ -1735,6 +1848,7 @@ export function ImageCanvasEditorView() {
|
||||
hoveredLayerId,
|
||||
canvasMarquee,
|
||||
canvasGenerationDialogs,
|
||||
isToolbarGuideVisible,
|
||||
taskListRefreshKey: generationSurface.taskListRefreshKey,
|
||||
isTaskSidebarOpen: generationSurface.isTaskSidebarOpen,
|
||||
generateDialog,
|
||||
|
||||
@@ -55,6 +55,7 @@ export type ImageCanvasStageViewProps = {
|
||||
hoveredLayerId: string | null;
|
||||
canvasMarquee: CanvasMarqueeState | null;
|
||||
canvasGenerationDialogs: CanvasGenerationDialogState[];
|
||||
isToolbarGuideVisible?: boolean;
|
||||
taskListRefreshKey: number;
|
||||
isTaskSidebarOpen: boolean;
|
||||
generateDialog: GenerateDialogState | null;
|
||||
@@ -194,6 +195,7 @@ export function ImageCanvasStageView({
|
||||
hoveredLayerId,
|
||||
canvasMarquee,
|
||||
canvasGenerationDialogs,
|
||||
isToolbarGuideVisible = false,
|
||||
taskListRefreshKey,
|
||||
isTaskSidebarOpen,
|
||||
generateDialog,
|
||||
@@ -444,11 +446,27 @@ export function ImageCanvasStageView({
|
||||
onFocusExternalTask={onFocusExternalTask}
|
||||
/>
|
||||
|
||||
{isToolbarGuideVisible ? (
|
||||
<div
|
||||
className="image-canvas-editor__toolbar-guide"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<img
|
||||
src="/branding/mobile-home-welcome-taonier-ip.png"
|
||||
alt=""
|
||||
className="image-canvas-editor__toolbar-guide-ip"
|
||||
/>
|
||||
<span>工具栏在这里,快试着随便做点什么</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ImageCanvasBottomToolbarView
|
||||
specToolWrapRef={specToolWrapRef}
|
||||
musicToolWrapRef={musicToolWrapRef}
|
||||
publicationToolWrapRef={publicationToolWrapRef}
|
||||
effectiveTool={effectiveTool}
|
||||
highlight={isToolbarGuideVisible}
|
||||
onSwitchTool={onSwitchTool}
|
||||
onOpenToolOptions={onOpenToolOptions}
|
||||
onCloseToolOptions={onCloseToolOptions}
|
||||
|
||||
@@ -3761,11 +3761,24 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsCreationCommunityOpen(true);
|
||||
}, []);
|
||||
|
||||
const openEditorProject = useCallback((projectId: string) => {
|
||||
setSelectionStage('image-editor', {
|
||||
path: `/editor/canvas?projectid=${encodeURIComponent(projectId)}`,
|
||||
});
|
||||
}, [setSelectionStage]);
|
||||
const openEditorProject = useCallback(
|
||||
(
|
||||
projectId: string,
|
||||
options?: { guide?: boolean; tool?: string },
|
||||
) => {
|
||||
const params = new URLSearchParams({ projectid: projectId });
|
||||
if (options?.tool) {
|
||||
params.set('tool', options.tool);
|
||||
}
|
||||
if (options?.guide) {
|
||||
params.set('guide', 'toolbar');
|
||||
}
|
||||
setSelectionStage('image-editor', {
|
||||
path: `/editor/canvas?${params.toString()}`,
|
||||
});
|
||||
},
|
||||
[setSelectionStage],
|
||||
);
|
||||
|
||||
const openCreationTypeModal = useCallback(() => {
|
||||
if (!prepareCreationLaunch()) {
|
||||
|
||||
@@ -431,6 +431,8 @@ describe('ProjectGalleryView', () => {
|
||||
await user.click(newProjectButton);
|
||||
|
||||
expect(createEditorProjectMock).toHaveBeenCalledTimes(1);
|
||||
expect(onOpenProject).toHaveBeenCalledWith('editor-project-new');
|
||||
expect(onOpenProject).toHaveBeenCalledWith('editor-project-new', {
|
||||
guide: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
} from './ProjectCanvasCover';
|
||||
|
||||
type ProjectGalleryViewProps = {
|
||||
onOpenProject: (projectId: string) => void;
|
||||
onOpenProject: (projectId: string, options?: { guide?: boolean }) => void;
|
||||
};
|
||||
|
||||
type RenameDraft = {
|
||||
@@ -171,7 +171,7 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
|
||||
}
|
||||
setErrorMessage(null);
|
||||
createEditorProject()
|
||||
.then((project) => onOpenProject(project.projectId))
|
||||
.then((project) => onOpenProject(project.projectId, { guide: true }))
|
||||
.catch((error: unknown) => {
|
||||
setErrorMessage(
|
||||
error instanceof Error ? error.message : '创建项目失败',
|
||||
|
||||
@@ -3576,6 +3576,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
}
|
||||
|
||||
.creation-landing__feature {
|
||||
appearance: none;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(5.6rem, 36%) minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
@@ -3588,9 +3589,33 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
radial-gradient(circle at 21% 42%, rgba(222, 130, 66, 0.12), transparent 9rem),
|
||||
linear-gradient(135deg, rgba(255, 254, 251, 0.92), rgba(255, 247, 238, 0.82));
|
||||
padding: 1.05rem 1.3rem;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
box-shadow:
|
||||
0 18px 36px rgba(124, 72, 35, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.72);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 160ms ease,
|
||||
box-shadow 160ms ease,
|
||||
transform 160ms ease;
|
||||
}
|
||||
|
||||
.creation-landing__feature:hover,
|
||||
.creation-landing__feature:focus-visible {
|
||||
border-color: rgba(209, 103, 54, 0.88);
|
||||
box-shadow:
|
||||
0 22px 42px rgba(124, 72, 35, 0.12),
|
||||
0 0 0 3px rgba(239, 130, 61, 0.16),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.78);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.creation-landing__feature:disabled {
|
||||
cursor: progress;
|
||||
opacity: 0.68;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.creation-landing__feature-figure {
|
||||
@@ -6802,6 +6827,58 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.image-canvas-editor__bottom-toolbar--guided {
|
||||
border-color: #ff4f4f;
|
||||
box-shadow:
|
||||
0 18px 38px rgba(15, 23, 42, 0.16),
|
||||
0 0 0 3px rgba(255, 79, 79, 0.24);
|
||||
}
|
||||
|
||||
.image-canvas-editor__toolbar-guide {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 4.45rem;
|
||||
z-index: 12;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0;
|
||||
max-width: min(28rem, calc(100% - 2rem));
|
||||
color: #ffffff;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.image-canvas-editor__toolbar-guide-ip {
|
||||
width: clamp(5.4rem, 12vw, 8.4rem);
|
||||
height: clamp(5.4rem, 12vw, 8.4rem);
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 16px 22px rgba(112, 57, 30, 0.18));
|
||||
}
|
||||
|
||||
.image-canvas-editor__toolbar-guide span {
|
||||
position: relative;
|
||||
margin-bottom: 0.9rem;
|
||||
margin-left: -0.55rem;
|
||||
border-radius: 0.9rem;
|
||||
background: #f28a2e;
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 850;
|
||||
line-height: 1.45;
|
||||
box-shadow: 0 16px 34px rgba(150, 78, 26, 0.2);
|
||||
}
|
||||
|
||||
.image-canvas-editor__toolbar-guide span::before {
|
||||
position: absolute;
|
||||
top: 1.35rem;
|
||||
left: -0.62rem;
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
background: #f28a2e;
|
||||
content: "";
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.image-canvas-editor__bottom-toolbar button[aria-pressed='true'] {
|
||||
border-color: var(--image-canvas-brand-border-strong);
|
||||
background: var(--image-canvas-brand-soft);
|
||||
@@ -9137,6 +9214,25 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.image-canvas-editor__toolbar-guide {
|
||||
right: 0.85rem;
|
||||
left: 0.85rem;
|
||||
bottom: 4.75rem;
|
||||
max-width: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.image-canvas-editor__toolbar-guide-ip {
|
||||
width: 5.25rem;
|
||||
height: 5.25rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__toolbar-guide span {
|
||||
margin-bottom: 0.65rem;
|
||||
padding: 0.82rem 0.95rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__panel-dock {
|
||||
left: 0.75rem;
|
||||
bottom: 3.85rem;
|
||||
|
||||
Reference in New Issue
Block a user