抽取项目页浮层菜单组件
新增 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 路由与数据归属修正:图片画布页面路由改为 `/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`。
|
||||||
|
|||||||
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';
|
} 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}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user