抽取项目页浮层菜单组件
新增 PlatformFloatingMenu 与菜单项测试 项目卡片更多菜单复用平台浮层菜单 更新 TRACKING 记录组件收口验证
This commit is contained in:
@@ -75,3 +75,4 @@
|
||||
- 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-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`。
|
||||
|
||||
31
src/components/common/PlatformFloatingMenu.test.tsx
Normal file
31
src/components/common/PlatformFloatingMenu.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
54
src/components/common/PlatformFloatingMenu.tsx
Normal file
54
src/components/common/PlatformFloatingMenu.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,10 @@ import {
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||
import {
|
||||
PlatformFloatingMenu,
|
||||
PlatformFloatingMenuItem,
|
||||
} from '../common/PlatformFloatingMenu';
|
||||
import { PlatformIconButton } from '../common/PlatformIconButton';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import { UnifiedModal } from '../common/UnifiedModal';
|
||||
@@ -242,10 +246,9 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
|
||||
}}
|
||||
/>
|
||||
{activeMenuProjectId === project.projectId ? (
|
||||
<div className="project-gallery__card-menu" role="menu">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
<PlatformFloatingMenu>
|
||||
<PlatformFloatingMenuItem
|
||||
icon={<Pencil className="h-4 w-4" />}
|
||||
onClick={() =>
|
||||
setRenameDraft({
|
||||
projectId: project.projectId,
|
||||
@@ -253,18 +256,15 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
|
||||
})
|
||||
}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
<span>重命名</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
重命名
|
||||
</PlatformFloatingMenuItem>
|
||||
<PlatformFloatingMenuItem
|
||||
icon={<Trash2 className="h-4 w-4" />}
|
||||
onClick={() => deleteProjects([project.projectId])}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</div>
|
||||
删除
|
||||
</PlatformFloatingMenuItem>
|
||||
</PlatformFloatingMenu>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -3126,7 +3126,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.project-gallery__card-menu {
|
||||
.platform-floating-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
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);
|
||||
}
|
||||
|
||||
.project-gallery__card-menu button {
|
||||
.platform-floating-menu__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
@@ -3152,7 +3152,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.project-gallery__card-menu button:hover {
|
||||
.platform-floating-menu__item:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user