diff --git a/TRACKING.md b/TRACKING.md index 34f12248..065be992 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -101,3 +101,4 @@ - 2026-06-14 组件复用修正:编辑器生成图右上角元数据角标改为复用 `PlatformIconButton asChild="spanButton"`,保留嵌套在图层按钮内的合法 DOM 结构,同时把 Enter / Space 键盘触发和 `darkMini` chrome 收口到共享组件;验证命令:`npm run test -- src/components/common/PlatformIconButton.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`。 - 2026-06-14 组件复用修正:编辑器画布内生成输入框和“修改图片”弹窗提示词改为复用 `PlatformTextField variant="textarea"`,删除编辑器里按标签选择器手写 textarea 基础输入 chrome 的做法,仅保留生成器局部尺寸和 Lovart 式覆盖;验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/common/PlatformTextField.test.tsx`、`npm run typecheck`。 - 2026-06-14 组件复用修正:新增 `PlatformInlineOptionButton`,编辑器生成输入框里的比例和模型选择 pill 改为复用平台内联选项按钮原语,删除两个局部按钮重复维护基础 inline chrome 的做法;验证命令:`npm run test -- src/components/common/PlatformInlineOptionButton.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`。 +- 2026-06-14 组件复用修正:`PlatformEmptyState` 增加 `asChild="button"` 形态,项目页空列表“新建项目”卡片改为复用平台空态原语,避免项目页单独维护可点击空态卡片基础 chrome;验证命令:`npm run test -- src/components/common/PlatformEmptyState.test.tsx src/components/project/ProjectGalleryView.test.tsx`、`npm run typecheck`。 diff --git a/src/components/common/PlatformEmptyState.test.tsx b/src/components/common/PlatformEmptyState.test.tsx index 5340792b..5cc55331 100644 --- a/src/components/common/PlatformEmptyState.test.tsx +++ b/src/components/common/PlatformEmptyState.test.tsx @@ -1,7 +1,7 @@ /* @vitest-environment jsdom */ -import { render, screen } from '@testing-library/react'; -import { expect, test } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { expect, test, vi } from 'vitest'; import { PlatformEmptyState } from './PlatformEmptyState'; @@ -71,3 +71,29 @@ test('supports dark editor dashed empty state', () => { expect(emptyState.className).toContain('bg-black/20'); expect(emptyState.className).toContain('text-[var(--platform-text-soft)]'); }); + +test('supports button empty state for empty collection actions', () => { + const onClick = vi.fn(); + render( + + 新建项目 + , + ); + + const button = screen.getByRole('button', { name: '新建项目' }); + + expect(button.tagName).toBe('BUTTON'); + expect(button.getAttribute('type')).toBe('button'); + expect(button.className).toContain('platform-empty-state'); + expect(button.className).toContain('local-empty-action'); + + fireEvent.click(button); + + expect(onClick).toHaveBeenCalledTimes(1); +}); diff --git a/src/components/common/PlatformEmptyState.tsx b/src/components/common/PlatformEmptyState.tsx index 25ae271f..a992c3d3 100644 --- a/src/components/common/PlatformEmptyState.tsx +++ b/src/components/common/PlatformEmptyState.tsx @@ -1,19 +1,41 @@ -import type { HTMLAttributes, ReactNode } from 'react'; +import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react'; type PlatformEmptyStateSurface = 'soft' | 'dashed' | 'subpanel' | 'editorDark'; type PlatformEmptyStateSize = 'compact' | 'panel' | 'inline'; type PlatformEmptyStateTone = 'base' | 'soft'; -type PlatformEmptyStateProps = Omit< - HTMLAttributes, - 'children' -> & { +type PlatformEmptyStateBaseProps = { children: ReactNode; surface?: PlatformEmptyStateSurface; size?: PlatformEmptyStateSize; tone?: PlatformEmptyStateTone; }; +type PlatformEmptyStateDivProps = Omit< + HTMLAttributes, + 'children' +> & { + asChild?: false; +} & PlatformEmptyStateBaseProps; + +type PlatformEmptyStateButtonProps = Omit< + ButtonHTMLAttributes, + 'children' +> & { + asChild: 'button'; +} & PlatformEmptyStateBaseProps; + +type PlatformEmptyStateProps = + | PlatformEmptyStateDivProps + | PlatformEmptyStateButtonProps; + +type PlatformEmptyStateClassNameOptions = { + surface: PlatformEmptyStateSurface; + size: PlatformEmptyStateSize; + tone: PlatformEmptyStateTone; + className?: string; +}; + const PLATFORM_EMPTY_STATE_SURFACE_CLASS: Record< PlatformEmptyStateSurface, string @@ -44,30 +66,57 @@ const PLATFORM_EMPTY_STATE_TONE_CLASS: Record = * 平台通用空态和轻量加载态。 * 收口平台列表、作品架和素材选择弹窗中重复的空面板外观。 */ +function getPlatformEmptyStateClassName({ + surface, + size, + tone, + className, +}: PlatformEmptyStateClassNameOptions) { + return [ + 'min-w-0', + 'platform-empty-state', + PLATFORM_EMPTY_STATE_SURFACE_CLASS[surface], + PLATFORM_EMPTY_STATE_SIZE_CLASS[size], + PLATFORM_EMPTY_STATE_TONE_CLASS[tone], + className, + ] + .filter(Boolean) + .join(' '); +} + export function PlatformEmptyState({ children, surface = 'soft', size = 'compact', tone, className, - ...divProps + asChild, + ...emptyStateProps }: PlatformEmptyStateProps) { const resolvedTone = tone ?? (surface === 'subpanel' || size === 'inline' ? 'soft' : 'base'); + const emptyStateClassName = getPlatformEmptyStateClassName({ + surface, + size, + tone: resolvedTone, + className, + }); + + if (asChild === 'button') { + const { type = 'button', ...buttonProps } = + emptyStateProps as ButtonHTMLAttributes; + + return ( + + ); + } return (
)} + className={emptyStateClassName} > {children}
diff --git a/src/components/project/ProjectGalleryView.test.tsx b/src/components/project/ProjectGalleryView.test.tsx index c64c177d..a3e8702b 100644 --- a/src/components/project/ProjectGalleryView.test.tsx +++ b/src/components/project/ProjectGalleryView.test.tsx @@ -134,4 +134,28 @@ describe('ProjectGalleryView', () => { expect(deleteEditorProjectMock).toHaveBeenCalledWith('editor-project-1'); expect(deleteEditorProjectMock).toHaveBeenCalledWith('editor-project-2'); }); + + it('renders the empty project action with shared empty state chrome', async () => { + const onOpenProject = vi.fn(); + listEditorProjectsMock.mockResolvedValueOnce([]); + createEditorProjectMock.mockResolvedValueOnce({ + ...projectItems[0], + projectId: 'editor-project-new', + }); + const user = userEvent.setup(); + + render(); + + const newProjectButton = await screen.findByRole('button', { + name: '新建项目', + }); + + expect(newProjectButton.className).toContain('platform-empty-state'); + expect(newProjectButton.className).toContain('project-gallery__new-card'); + + await user.click(newProjectButton); + + expect(createEditorProjectMock).toHaveBeenCalledTimes(1); + expect(onOpenProject).toHaveBeenCalledWith('editor-project-new'); + }); }); diff --git a/src/components/project/ProjectGalleryView.tsx b/src/components/project/ProjectGalleryView.tsx index ec3c6011..a52e80ed 100644 --- a/src/components/project/ProjectGalleryView.tsx +++ b/src/components/project/ProjectGalleryView.tsx @@ -327,14 +327,16 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) { 正在读取项目 ) : projects.length === 0 ? ( - + ) : (
{projectCards}
)}