From d35595c1ea74d0459f4a5d66b31d0d5a30de6f54 Mon Sep 17 00:00:00 2001 From: totoro Date: Fri, 24 Jul 2026 19:36:41 +0800 Subject: [PATCH] fix: make new project action reset dashboard composer --- src/features/dashboard/dashboard-lib.test.ts | 23 +++++++++++++- src/features/dashboard/dashboard-lib.ts | 26 +++++++++++++++ src/features/dashboard/dashboard-page.tsx | 33 +++++++++++++++----- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/features/dashboard/dashboard-lib.test.ts b/src/features/dashboard/dashboard-lib.test.ts index bbc417c..10fa0f3 100644 --- a/src/features/dashboard/dashboard-lib.test.ts +++ b/src/features/dashboard/dashboard-lib.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from 'vitest' -import { THEME_PREFERENCE_CHIPS, THEME_PREFERENCES } from './dashboard-lib' +import { + THEME_PREFERENCE_CHIPS, + THEME_PREFERENCES, + createNewProjectComposerState, + isNewProjectActionDisabled, +} from './dashboard-lib' describe('dashboard theme preferences', () => { it('exposes only explicit theme preferences instead of business templates', () => { @@ -22,3 +27,19 @@ describe('dashboard theme preferences', () => { }) }) }) + +describe('dashboard new project action', () => { + it('resets the composer to a clean draft with the default theme', () => { + expect(createNewProjectComposerState()).toEqual({ + prompt: '', + inputError: null, + selectedTheme: THEME_PREFERENCE_CHIPS[0].id, + }) + }) + + it('is disabled while projects are loading or a project is being created', () => { + expect(isNewProjectActionDisabled({ creating: false, loading: true })).toBe(true) + expect(isNewProjectActionDisabled({ creating: true, loading: false })).toBe(true) + expect(isNewProjectActionDisabled({ creating: false, loading: false })).toBe(false) + }) +}) diff --git a/src/features/dashboard/dashboard-lib.ts b/src/features/dashboard/dashboard-lib.ts index 47bba65..2bd0175 100644 --- a/src/features/dashboard/dashboard-lib.ts +++ b/src/features/dashboard/dashboard-lib.ts @@ -24,6 +24,32 @@ export const THEME_PREFERENCE_CHIPS = [ export type ThemePreferenceId = (typeof THEME_PREFERENCE_CHIPS)[number]['id'] +export type NewProjectComposerState = { + prompt: string + inputError: null + selectedTheme: ThemePreferenceId +} + +/** 新建项目入口始终回到干净的 Composer 草稿。 */ +export function createNewProjectComposerState(): NewProjectComposerState { + return { + prompt: '', + inputError: null, + selectedTheme: THEME_PREFERENCE_CHIPS[0].id, + } +} + +/** 本地项目尚未加载完成或已有创建任务时,不允许再次开始新项目。 */ +export function isNewProjectActionDisabled({ + creating, + loading, +}: { + creating: boolean + loading: boolean +}): boolean { + return creating || loading +} + /** * 主题选项 → PlannerInput.themePreference 映射。当前只提供舒适密度, * 与产品规格默认的 neutral / comfortable 保持一致;颜色预设由用户明确选择。 diff --git a/src/features/dashboard/dashboard-page.tsx b/src/features/dashboard/dashboard-page.tsx index 9bcd747..920d5a3 100644 --- a/src/features/dashboard/dashboard-page.tsx +++ b/src/features/dashboard/dashboard-page.tsx @@ -22,7 +22,9 @@ import { THEME_PREFERENCES, THEME_PREFERENCE_CHIPS, TOO_LONG_INPUT_MESSAGE, + createNewProjectComposerState, deriveTitleFromPrompt, + isNewProjectActionDisabled, type ThemePreferenceId, } from './dashboard-lib' import { ExamplePromptList } from './example-prompt-list' @@ -55,6 +57,7 @@ export function DashboardPage() { const textareaRef = useRef(null) const menuButtonRef = useRef(null) + const restoreMenuFocusRef = useRef(true) const loadProjects = useCallback(async () => { setLoadState('loading') @@ -119,13 +122,24 @@ export function DashboardPage() { window.addEventListener('keydown', handleKeyDown) return () => { window.removeEventListener('keydown', handleKeyDown) - menuButtonRef.current?.focus() + if (restoreMenuFocusRef.current) { + menuButtonRef.current?.focus() + } + restoreMenuFocusRef.current = true } }, [drawerOpen]) - const focusComposer = useCallback(() => { + const handleNewProject = useCallback(() => { + const nextComposer = createNewProjectComposerState() + setPrompt(nextComposer.prompt) + setInputError(nextComposer.inputError) + setSelectedTheme(nextComposer.selectedTheme) window.scrollTo({ top: 0, behavior: 'smooth' }) - textareaRef.current?.focus() + + // 等待状态更新和移动端抽屉卸载完成,再把焦点交给 Composer。 + window.requestAnimationFrame(() => { + textareaRef.current?.focus() + }) }, []) const handlePickExample = useCallback( @@ -184,6 +198,10 @@ export function DashboardPage() { updatedAt: project.updatedAt, phase: project.phase, })) + const newProjectDisabled = isNewProjectActionDisabled({ + creating, + loading: loadState === 'loading', + }) return ( <> @@ -213,8 +231,8 @@ export function DashboardPage() { {drawerOpen ? ( @@ -223,10 +241,11 @@ export function DashboardPage() { { + restoreMenuFocusRef.current = false setDrawerOpen(false) - focusComposer() + handleNewProject() }} variant="drawer" onNavigate={() => setDrawerOpen(false)}