抽取项目页浮层菜单组件

新增 PlatformFloatingMenu 与菜单项测试

项目卡片更多菜单复用平台浮层菜单

更新 TRACKING 记录组件收口验证
This commit is contained in:
2026-06-14 00:50:05 +08:00
parent 8b4175dc7d
commit 304d6806f0
5 changed files with 103 additions and 17 deletions

View File

@@ -75,3 +75,4 @@
- 2026-06-13 路由与数据归属修正:图片画布页面路由改为 `/editor/canvas`;新增 `editor_canvas` 表作为 project 下的画布数据表,当前工程创建时同步创建默认画布,保存 layout 时写入默认画布API 响应同时返回 `project.canvas` 和兼容顶层 `viewport/layers` - 2026-06-13 路由与数据归属修正:图片画布页面路由改为 `/editor/canvas`;新增 `editor_canvas` 表作为 project 下的画布数据表,当前工程创建时同步创建默认画布,保存 layout 时写入默认画布API 响应同时返回 `project.canvas` 和兼容顶层 `viewport/layers`
- 2026-06-13 项目页修正:新增 `/project` 作为图片画布工程列表入口;从“我的”页进入项目页,项目卡片进入 `/editor/canvas?projectid=<projectId>`,并补齐项目列表、重命名、删除和批量删除 API。`GET /api/editor/projects` 在重启后的 api-server 上返回未登录 401不再是旧进程的 405`/project` 前端路由 smoke 可渲染项目页白底布局。 - 2026-06-13 项目页修正:新增 `/project` 作为图片画布工程列表入口;从“我的”页进入项目页,项目卡片进入 `/editor/canvas?projectid=<projectId>`,并补齐项目列表、重命名、删除和批量删除 API。`GET /api/editor/projects` 在重启后的 api-server 上返回未登录 401不再是旧进程的 405`/project` 前端路由 smoke 可渲染项目页白底布局。
- 2026-06-14 组件复用修正:项目页重命名弹窗改为复用 `UnifiedModal``PlatformTextField``PlatformActionButton`,删除项目页局部 modal / input 样式,避免同类弹窗和表单 chrome 重复实现。 - 2026-06-14 组件复用修正:项目页重命名弹窗改为复用 `UnifiedModal``PlatformTextField``PlatformActionButton`,删除项目页局部 modal / input 样式,避免同类弹窗和表单 chrome 重复实现。
- 2026-06-14 组件复用修正:新增 `PlatformFloatingMenu` / `PlatformFloatingMenuItem`,项目卡片右下角更多菜单改为复用平台浮层菜单原语;验证命令:`npm run test -- src/components/common/PlatformFloatingMenu.test.tsx src/components/project/ProjectGalleryView.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`

View File

@@ -0,0 +1,31 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import {
PlatformFloatingMenu,
PlatformFloatingMenuItem,
} from './PlatformFloatingMenu';
describe('PlatformFloatingMenu', () => {
it('renders menu items with accessible menu semantics', async () => {
const onRename = vi.fn();
const user = userEvent.setup();
render(
<PlatformFloatingMenu>
<PlatformFloatingMenuItem onClick={onRename}>
</PlatformFloatingMenuItem>
</PlatformFloatingMenu>,
);
expect(screen.getByRole('menu')).toBeTruthy();
await user.click(screen.getByRole('menuitem', { name: '重命名' }));
expect(onRename).toHaveBeenCalledOnce();
});
});

View File

@@ -0,0 +1,54 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react';
type PlatformFloatingMenuProps = {
children: ReactNode;
className?: string;
};
type PlatformFloatingMenuItemProps = Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
'children'
> & {
icon?: ReactNode;
children: ReactNode;
};
/**
* 平台浮层菜单原语。
* 用于卡片角标、画布局部菜单等轻量动作集合,统一 role 与菜单项 chrome。
*/
export function PlatformFloatingMenu({
children,
className,
}: PlatformFloatingMenuProps) {
return (
<div
className={['platform-floating-menu', className].filter(Boolean).join(' ')}
role="menu"
>
{children}
</div>
);
}
export function PlatformFloatingMenuItem({
icon,
children,
className,
type = 'button',
...buttonProps
}: PlatformFloatingMenuItemProps) {
return (
<button
{...buttonProps}
type={type}
className={['platform-floating-menu__item', className]
.filter(Boolean)
.join(' ')}
role="menuitem"
>
{icon}
<span>{children}</span>
</button>
);
}

View File

@@ -18,6 +18,10 @@ import {
} from '../../services/image-editor/editorProjectClient'; } from '../../services/image-editor/editorProjectClient';
import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformEmptyState } from '../common/PlatformEmptyState';
import {
PlatformFloatingMenu,
PlatformFloatingMenuItem,
} from '../common/PlatformFloatingMenu';
import { PlatformIconButton } from '../common/PlatformIconButton'; import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformTextField } from '../common/PlatformTextField'; import { PlatformTextField } from '../common/PlatformTextField';
import { UnifiedModal } from '../common/UnifiedModal'; import { UnifiedModal } from '../common/UnifiedModal';
@@ -242,10 +246,9 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
}} }}
/> />
{activeMenuProjectId === project.projectId ? ( {activeMenuProjectId === project.projectId ? (
<div className="project-gallery__card-menu" role="menu"> <PlatformFloatingMenu>
<button <PlatformFloatingMenuItem
type="button" icon={<Pencil className="h-4 w-4" />}
role="menuitem"
onClick={() => onClick={() =>
setRenameDraft({ setRenameDraft({
projectId: project.projectId, projectId: project.projectId,
@@ -253,18 +256,15 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
}) })
} }
> >
<Pencil className="h-4 w-4" />
<span></span> </PlatformFloatingMenuItem>
</button> <PlatformFloatingMenuItem
<button icon={<Trash2 className="h-4 w-4" />}
type="button"
role="menuitem"
onClick={() => deleteProjects([project.projectId])} onClick={() => deleteProjects([project.projectId])}
> >
<Trash2 className="h-4 w-4" />
<span></span> </PlatformFloatingMenuItem>
</button> </PlatformFloatingMenu>
</div>
) : null} ) : null}
</div> </div>
) : null} ) : null}

View File

@@ -3126,7 +3126,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
opacity: 1; opacity: 1;
} }
.project-gallery__card-menu { .platform-floating-menu {
position: absolute; position: absolute;
right: 0; right: 0;
bottom: 2.4rem; bottom: 2.4rem;
@@ -3139,7 +3139,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.16); box-shadow: 0 18px 40px rgba(15, 23, 42, 0.16);
} }
.project-gallery__card-menu button { .platform-floating-menu__item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.45rem; gap: 0.45rem;
@@ -3152,7 +3152,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
text-align: left; text-align: left;
} }
.project-gallery__card-menu button:hover { .platform-floating-menu__item:hover {
background: #f3f4f6; background: #f3f4f6;
} }