Files
Genarrative/src/components/image-editor/EditorAgentConversation/MessageBubbleRightClickMenu.tsx
T
k88936 e6b3199490
Project CI / Repository checks (push) Successful in 48s
Project CI / Frontend tests (push) Successful in 3m21s
Project CI / Backend tests (push) Successful in 3m53s
Project CI / Native shell tests (push) Successful in 11m16s
Fix/统一修复modal样式失效 (#122)
之前遇到几次弹窗的样式不生效,bug 现在在画布agent的删除对话弹窗又出现了,
这个pr把类似的修改集中到最下层的UnifiedModal,适用于所有的modal,
一些特殊的modal有特殊处理.

画布agent的删除对话弹窗
![shotmd-1785399342.jpg](/attachments/d63d7dc6-bf7e-4f13-bfe8-bc4716cd798f)

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/122
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-08-03 17:55:47 +08:00

273 lines
8.1 KiB
TypeScript

import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import type { ImageCanvasActionResult } from '@/src/components/image-editor/ImageCanvasActionsContext.ts';
import { ImageCanvasEditorPortal } from '../ImageCanvasEditorPortal.tsx';
import {
contextAssetMediaSrc,
type EditorAgentContextAsset,
EditorAgentRightClickAction,
type RightClickMenuTarget,
} from './common.ts';
type MessageBubbleRightClickMenuProps = {
x: number;
y: number;
target: RightClickMenuTarget;
pendingAction: EditorAgentRightClickAction | null;
resultAction: EditorAgentRightClickAction | null;
result: ImageCanvasActionResult | null;
onAction: (action: EditorAgentRightClickAction) => void;
onClose: () => void;
};
const VIEWPORT_MARGIN = 8;
function actionLabel({
action,
idleLabel,
pendingAction,
resultAction,
result,
}: {
action: EditorAgentRightClickAction;
idleLabel: string;
pendingAction: EditorAgentRightClickAction | null;
resultAction: EditorAgentRightClickAction | null;
result: ImageCanvasActionResult | null;
}) {
if (pendingAction === action) {
switch (action) {
case EditorAgentRightClickAction.FocusCanvas:
return '定位中';
case EditorAgentRightClickAction.DownloadAsset:
return '下载中';
case EditorAgentRightClickAction.ReferenceImage:
return '引用中';
case EditorAgentRightClickAction.CopyText:
case EditorAgentRightClickAction.CopyImage:
return '复制中';
}
}
const actionResult = resultAction === action ? result : null;
if (!actionResult) {
return idleLabel;
}
switch (action) {
case EditorAgentRightClickAction.FocusCanvas:
if (actionResult.successed) return '已定位';
switch (actionResult.reason) {
case 'not-found-on-canva':
return '画布上不存在';
case 'other':
default:
return '失败';
}
case EditorAgentRightClickAction.DownloadAsset:
if (actionResult.successed) return '已下载';
return '下载失败';
case EditorAgentRightClickAction.ReferenceImage:
if (actionResult.successed) return '已引用';
return '引用失败';
case EditorAgentRightClickAction.CopyText:
case EditorAgentRightClickAction.CopyImage:
if (actionResult.successed) return '已复制';
return '复制失败';
}
}
function assetDownloadLabel(asset: EditorAgentContextAsset) {
return `下载${
asset.mediaType === 'image'
? '图片'
: asset.mediaType === 'video'
? '视频'
: '音频'
}`;
}
export function MessageBubbleRightClickMenu({
x,
y,
target,
pendingAction,
resultAction,
result,
onAction,
onClose,
}: MessageBubbleRightClickMenuProps) {
const menuRef = useRef<HTMLDivElement | null>(null);
const [position, setPosition] = useState<{ x: number; y: number } | null>(
null,
);
useLayoutEffect(() => {
const menu = menuRef.current;
if (!menu || typeof window === 'undefined') {
return;
}
const rect = menu.getBoundingClientRect();
setPosition({
x: Math.min(
Math.max(x, VIEWPORT_MARGIN),
Math.max(
VIEWPORT_MARGIN,
window.innerWidth - rect.width - VIEWPORT_MARGIN,
),
),
y: Math.min(
Math.max(y, VIEWPORT_MARGIN),
Math.max(
VIEWPORT_MARGIN,
window.innerHeight - rect.height - VIEWPORT_MARGIN,
),
),
});
}, [target.kind, x, y]);
useEffect(() => {
const handlePointerDown = (event: PointerEvent) => {
if (!menuRef.current?.contains(event.target as Node)) {
onClose();
}
};
const handleContextMenu = (event: MouseEvent) => {
if (menuRef.current?.contains(event.target as Node)) {
event.preventDefault();
}
onClose();
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
window.addEventListener('pointerdown', handlePointerDown);
window.addEventListener('contextmenu', handleContextMenu, true);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('scroll', onClose, true);
window.addEventListener('resize', onClose);
return () => {
window.removeEventListener('pointerdown', handlePointerDown);
window.removeEventListener('contextmenu', handleContextMenu, true);
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('scroll', onClose, true);
window.removeEventListener('resize', onClose);
};
}, [onClose]);
return (
<ImageCanvasEditorPortal>
<div
ref={menuRef}
className="image-canvas-editor__context-menu"
role="menu"
aria-label={
target.kind === 'text' ? '消息右键菜单' : '消息素材右键菜单'
}
style={{
left: position?.x ?? x,
top: position?.y ?? y,
zIndex: 60,
}}
onContextMenu={(event) => event.preventDefault()}
>
{target.kind === 'text' ? (
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() => onAction(EditorAgentRightClickAction.CopyText)}
>
{actionLabel({
action: EditorAgentRightClickAction.CopyText,
idleLabel: '复制文本',
pendingAction,
resultAction,
result,
})}
</button>
) : (
<>
{target.asset.kind === 'generated_media' &&
target.asset.resourceId?.trim() ? (
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() =>
onAction(EditorAgentRightClickAction.FocusCanvas)
}
>
{actionLabel({
action: EditorAgentRightClickAction.FocusCanvas,
idleLabel: '在画布中定位',
pendingAction,
resultAction,
result,
})}
</button>
) : null}
{target.asset.mediaType === 'image' ? (
<>
{contextAssetMediaSrc(target.asset).trim() ? (
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() =>
onAction(EditorAgentRightClickAction.ReferenceImage)
}
>
{actionLabel({
action: EditorAgentRightClickAction.ReferenceImage,
idleLabel: '引用',
pendingAction,
resultAction,
result,
})}
</button>
) : null}
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() =>
onAction(EditorAgentRightClickAction.CopyImage)
}
>
{actionLabel({
action: EditorAgentRightClickAction.CopyImage,
idleLabel: '复制图片',
pendingAction,
resultAction,
result,
})}
</button>
</>
) : null}
<button
type="button"
role="menuitem"
disabled={pendingAction !== null}
onClick={() =>
onAction(EditorAgentRightClickAction.DownloadAsset)
}
>
{actionLabel({
action: EditorAgentRightClickAction.DownloadAsset,
idleLabel: assetDownloadLabel(target.asset),
pendingAction,
resultAction,
result,
})}
</button>
</>
)}
</div>
</ImageCanvasEditorPortal>
);
}