fix: stabilize video canvas quick connect
This commit is contained in:
parent
e054dcabcc
commit
3ad76b5c54
@ -120,9 +120,6 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@ -274,6 +271,8 @@ interface VideoCanvasProjectExportPayload {
|
|||||||
interface VideoCanvasInsertMenuState {
|
interface VideoCanvasInsertMenuState {
|
||||||
screenPosition: Position;
|
screenPosition: Position;
|
||||||
canvasPosition: Position;
|
canvasPosition: Position;
|
||||||
|
sourceNodeId?: string | null;
|
||||||
|
openLeft?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoCanvasInsertMenuChild {
|
interface VideoCanvasInsertMenuChild {
|
||||||
@ -1430,6 +1429,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
});
|
});
|
||||||
const [videoCanvasInsertMenu, setVideoCanvasInsertMenu] =
|
const [videoCanvasInsertMenu, setVideoCanvasInsertMenu] =
|
||||||
useState<VideoCanvasInsertMenuState | null>(null);
|
useState<VideoCanvasInsertMenuState | null>(null);
|
||||||
|
const videoCanvasInsertMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [isVideoCanvasFileListOpen, setIsVideoCanvasFileListOpen] =
|
const [isVideoCanvasFileListOpen, setIsVideoCanvasFileListOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [isVideoCanvasMarkerPanelOpen, setIsVideoCanvasMarkerPanelOpen] =
|
const [isVideoCanvasMarkerPanelOpen, setIsVideoCanvasMarkerPanelOpen] =
|
||||||
@ -2708,6 +2708,31 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
return () => window.removeEventListener("resize", updateViewportWidth);
|
return () => window.removeEventListener("resize", updateViewportWidth);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoCanvasInsertMenu) return;
|
||||||
|
|
||||||
|
const handlePointerDown = (event: PointerEvent) => {
|
||||||
|
const menu = videoCanvasInsertMenuRef.current;
|
||||||
|
if (menu && event.target instanceof Node && menu.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setVideoCanvasInsertMenu(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
setVideoCanvasInsertMenu(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("pointerdown", handlePointerDown);
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("pointerdown", handlePointerDown);
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [videoCanvasInsertMenu]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAgentMode) {
|
if (isAgentMode) {
|
||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
@ -5714,15 +5739,18 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
nodeConfig: Parameters<typeof addNode>[0],
|
nodeConfig: Parameters<typeof addNode>[0],
|
||||||
options?: {
|
options?: {
|
||||||
connectFromSelection?: boolean;
|
connectFromSelection?: boolean;
|
||||||
|
connectFromNodeId?: string | null;
|
||||||
selectNewNode?: boolean;
|
selectNewNode?: boolean;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const connectFromSelection = options?.connectFromSelection ?? true;
|
const connectFromSelection = options?.connectFromSelection ?? true;
|
||||||
const selectNewNode = options?.selectNewNode ?? true;
|
const selectNewNode = options?.selectNewNode ?? true;
|
||||||
|
const explicitSourceNodeId = options?.connectFromNodeId || null;
|
||||||
const nodeId = addNode(nodeConfig);
|
const nodeId = addNode(nodeConfig);
|
||||||
const sourceNodeId =
|
const sourceNodeId =
|
||||||
connectFromSelection && isVideoMode && selectedNodeIds.length === 1
|
connectFromSelection && isVideoMode
|
||||||
? selectedNodeIds[0] || null
|
? explicitSourceNodeId ||
|
||||||
|
(selectedNodeIds.length === 1 ? selectedNodeIds[0] || null : null)
|
||||||
: null;
|
: null;
|
||||||
if (sourceNodeId && sourceNodeId !== nodeId) {
|
if (sourceNodeId && sourceNodeId !== nodeId) {
|
||||||
addEdge({
|
addEdge({
|
||||||
@ -5745,6 +5773,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
options?: {
|
options?: {
|
||||||
position?: Position;
|
position?: Position;
|
||||||
connectFromSelection?: boolean;
|
connectFromSelection?: boolean;
|
||||||
|
connectFromNodeId?: string | null;
|
||||||
selectNewNode?: boolean;
|
selectNewNode?: boolean;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
@ -5754,6 +5783,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
options?.position || getVideoWorkflowPlacement(definition.size);
|
options?.position || getVideoWorkflowPlacement(definition.size);
|
||||||
createCanvasNode(buildVideoWorkflowCanvasNode(templateType, placement), {
|
createCanvasNode(buildVideoWorkflowCanvasNode(templateType, placement), {
|
||||||
connectFromSelection: options?.connectFromSelection ?? true,
|
connectFromSelection: options?.connectFromSelection ?? true,
|
||||||
|
connectFromNodeId: options?.connectFromNodeId ?? null,
|
||||||
selectNewNode: options?.selectNewNode ?? true,
|
selectNewNode: options?.selectNewNode ?? true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -5773,7 +5803,21 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
if (!videoCanvasInsertMenu) return;
|
if (!videoCanvasInsertMenu) return;
|
||||||
|
|
||||||
const definition = getVideoWorkflowNodeDefinition(templateType);
|
const definition = getVideoWorkflowNodeDefinition(templateType);
|
||||||
const anchoredPosition = definition
|
const sourceNode = videoCanvasInsertMenu.sourceNodeId
|
||||||
|
? nodes.find((node) => node.id === videoCanvasInsertMenu.sourceNodeId)
|
||||||
|
: null;
|
||||||
|
const anchoredPosition =
|
||||||
|
sourceNode && definition
|
||||||
|
? {
|
||||||
|
x: videoCanvasInsertMenu.openLeft
|
||||||
|
? sourceNode.position.x - definition.size.width - 180
|
||||||
|
: sourceNode.position.x + sourceNode.size.width + 180,
|
||||||
|
y:
|
||||||
|
sourceNode.position.y +
|
||||||
|
sourceNode.size.height / 2 -
|
||||||
|
definition.size.height / 2,
|
||||||
|
}
|
||||||
|
: definition
|
||||||
? {
|
? {
|
||||||
x:
|
x:
|
||||||
videoCanvasInsertMenu.canvasPosition.x -
|
videoCanvasInsertMenu.canvasPosition.x -
|
||||||
@ -5786,11 +5830,12 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
|
|
||||||
handleAddWorkflowNode(templateType, {
|
handleAddWorkflowNode(templateType, {
|
||||||
position: anchoredPosition,
|
position: anchoredPosition,
|
||||||
connectFromSelection: false,
|
connectFromSelection: Boolean(sourceNode),
|
||||||
|
connectFromNodeId: sourceNode?.id || null,
|
||||||
});
|
});
|
||||||
setVideoCanvasInsertMenu(null);
|
setVideoCanvasInsertMenu(null);
|
||||||
},
|
},
|
||||||
[handleAddWorkflowNode, videoCanvasInsertMenu]
|
[handleAddWorkflowNode, nodes, videoCanvasInsertMenu]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateSelectedAgentResultBranches = useCallback(async () => {
|
const handleCreateSelectedAgentResultBranches = useCallback(async () => {
|
||||||
@ -8574,125 +8619,120 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isVideoMode && videoCanvasInsertMenu ? (
|
{isVideoMode && videoCanvasInsertMenu
|
||||||
<DropdownMenu
|
? (() => {
|
||||||
modal={false}
|
const menuWidth = 312;
|
||||||
onOpenChange={(open) => {
|
const menuLeft =
|
||||||
if (!open) {
|
viewportWidth > 0
|
||||||
setVideoCanvasInsertMenu(null);
|
? Math.max(
|
||||||
}
|
12,
|
||||||
}}
|
Math.min(
|
||||||
open={Boolean(videoCanvasInsertMenu)}
|
videoCanvasInsertMenu.screenPosition.x,
|
||||||
>
|
Math.max(12, viewportWidth - menuWidth - 12)
|
||||||
<DropdownMenuTrigger asChild>
|
)
|
||||||
<button
|
)
|
||||||
aria-hidden="true"
|
: videoCanvasInsertMenu.screenPosition.x;
|
||||||
className="pointer-events-none fixed h-px w-px opacity-0"
|
const menuTop = Math.max(
|
||||||
style={{
|
12,
|
||||||
left: `${videoCanvasInsertMenu.screenPosition.x}px`,
|
videoCanvasInsertMenu.screenPosition.y + 14
|
||||||
top: `${videoCanvasInsertMenu.screenPosition.y}px`,
|
);
|
||||||
}}
|
const renderChildButton = (child: VideoCanvasInsertMenuChild) => (
|
||||||
tabIndex={-1}
|
<button
|
||||||
type="button"
|
className="flex w-full items-center rounded-xl px-3 py-2 text-left font-medium text-[13px] text-slate-600 transition-colors hover:bg-slate-50 hover:text-slate-950 dark:text-slate-200 dark:hover:bg-zinc-900"
|
||||||
/>
|
key={child.id}
|
||||||
</DropdownMenuTrigger>
|
onClick={() =>
|
||||||
<DropdownMenuContent
|
handleInsertWorkflowNodeFromMenu(child.templateType)
|
||||||
align="start"
|
}
|
||||||
className="w-[312px] rounded-[28px] border border-slate-200 bg-white p-2.5 text-foreground shadow-[0_28px_80px_-32px_rgba(15,23,42,0.35)] dark:border-zinc-800 dark:bg-zinc-950"
|
type="button"
|
||||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
>
|
||||||
sideOffset={14}
|
{child.label}
|
||||||
>
|
</button>
|
||||||
<div className="px-4 py-3 font-semibold text-[14px] text-slate-500 dark:text-zinc-400">
|
);
|
||||||
@{siteSettingsData?.siteName || "视频画布"}
|
const renderInsertItem = (
|
||||||
</div>
|
item: VideoCanvasInsertMenuItem,
|
||||||
|
highlighted = false
|
||||||
{VIDEO_CANVAS_INSERT_MENU_ITEMS.slice(0, -1).map((item) => {
|
) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
|
const textClassName = highlighted
|
||||||
|
? "text-orange-600 dark:text-orange-300"
|
||||||
|
: "text-slate-700 dark:text-slate-100";
|
||||||
|
const iconClassName = highlighted
|
||||||
|
? "text-orange-500"
|
||||||
|
: "text-slate-400 dark:text-zinc-500";
|
||||||
|
|
||||||
if (item.children?.length) {
|
if (item.children?.length) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuSub key={item.id}>
|
<div className="rounded-2xl" key={item.id}>
|
||||||
<DropdownMenuSubTrigger className="rounded-2xl px-4 py-3 font-medium text-[15px] text-slate-700 focus:bg-slate-50 data-[state=open]:bg-slate-50 dark:text-slate-100 dark:data-[state=open]:bg-zinc-900 dark:focus:bg-zinc-900">
|
<div
|
||||||
<Icon className="h-5 w-5 text-slate-400 dark:text-zinc-500" />
|
className={cn(
|
||||||
<span>{item.label}</span>
|
"flex items-center gap-3 rounded-2xl px-4 py-3 font-semibold text-[15px]",
|
||||||
</DropdownMenuSubTrigger>
|
textClassName
|
||||||
<DropdownMenuSubContent className="min-w-[220px] rounded-2xl border border-slate-200 bg-white p-1.5 shadow-[0_22px_60px_-28px_rgba(15,23,42,0.34)] dark:border-zinc-800 dark:bg-zinc-950">
|
)}
|
||||||
{item.children.map((child) => (
|
>
|
||||||
<DropdownMenuItem
|
<Icon className={cn("h-5 w-5", iconClassName)} />
|
||||||
className="rounded-xl px-3 py-2.5 font-medium text-slate-700 text-sm focus:bg-slate-50 dark:text-slate-100 dark:focus:bg-zinc-900"
|
<span className="flex-1">{item.label}</span>
|
||||||
key={child.id}
|
<ChevronRight className="h-4 w-4 opacity-60" />
|
||||||
onSelect={() =>
|
</div>
|
||||||
handleInsertWorkflowNodeFromMenu(
|
<div className="ml-8 mr-2 mb-1 grid gap-0.5 border-slate-100 border-l pl-2 dark:border-zinc-800">
|
||||||
child.templateType
|
{item.children.map(renderChildButton)}
|
||||||
)
|
</div>
|
||||||
}
|
</div>
|
||||||
>
|
|
||||||
{child.label}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuSubContent>
|
|
||||||
</DropdownMenuSub>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<button
|
||||||
className="rounded-2xl px-4 py-3 font-medium text-[15px] text-slate-700 focus:bg-slate-50 dark:text-slate-100 dark:focus:bg-zinc-900"
|
className={cn(
|
||||||
|
"flex w-full items-center gap-3 rounded-2xl px-4 py-3 text-left font-medium text-[15px] transition-colors hover:bg-slate-50 dark:hover:bg-zinc-900",
|
||||||
|
textClassName
|
||||||
|
)}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onSelect={() =>
|
onClick={() =>
|
||||||
item.templateType &&
|
item.templateType &&
|
||||||
handleInsertWorkflowNodeFromMenu(item.templateType)
|
handleInsertWorkflowNodeFromMenu(item.templateType)
|
||||||
}
|
}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5 text-slate-400 dark:text-zinc-500" />
|
<Icon className={cn("h-5 w-5", iconClassName)} />
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</DropdownMenuItem>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
};
|
||||||
|
const highlightedItem = VIDEO_CANVAS_INSERT_MENU_ITEMS.at(-1);
|
||||||
|
|
||||||
<DropdownMenuSeparator className="mx-3 my-2 bg-slate-200 dark:bg-zinc-800" />
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed z-[70] max-h-[calc(100vh-24px)] overflow-y-auto rounded-[28px] border border-slate-200 bg-white p-2.5 text-foreground shadow-[0_28px_80px_-32px_rgba(15,23,42,0.35)] dark:border-zinc-800 dark:bg-zinc-950"
|
||||||
|
data-canvas-overlay="true"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
|
ref={videoCanvasInsertMenuRef}
|
||||||
|
style={{
|
||||||
|
left: `${menuLeft}px`,
|
||||||
|
top: `${menuTop}px`,
|
||||||
|
width: `${menuWidth}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="px-4 py-3 font-semibold text-[14px] text-slate-500 dark:text-zinc-400">
|
||||||
|
@{siteSettingsData?.siteName || "视频画布"}
|
||||||
|
</div>
|
||||||
|
|
||||||
{(() => {
|
<div className="grid gap-0.5">
|
||||||
const highlightedItem = VIDEO_CANVAS_INSERT_MENU_ITEMS.at(-1);
|
{VIDEO_CANVAS_INSERT_MENU_ITEMS.slice(0, -1).map((item) =>
|
||||||
if (!highlightedItem) return null;
|
renderInsertItem(item)
|
||||||
const HighlightIcon = highlightedItem.icon;
|
)}
|
||||||
return highlightedItem.children?.length ? (
|
</div>
|
||||||
<DropdownMenuSub>
|
|
||||||
<DropdownMenuSubTrigger className="rounded-2xl px-4 py-3 font-semibold text-[15px] text-orange-600 focus:bg-orange-50 data-[state=open]:bg-orange-50 dark:text-orange-300 dark:data-[state=open]:bg-orange-950/40 dark:focus:bg-orange-950/40">
|
{highlightedItem ? (
|
||||||
<HighlightIcon className="h-5 w-5 text-orange-500" />
|
<>
|
||||||
<span>{highlightedItem.label}</span>
|
<div className="mx-3 my-2 h-px bg-slate-200 dark:bg-zinc-800" />
|
||||||
</DropdownMenuSubTrigger>
|
{renderInsertItem(highlightedItem, true)}
|
||||||
<DropdownMenuSubContent className="min-w-[220px] rounded-2xl border border-slate-200 bg-white p-1.5 shadow-[0_22px_60px_-28px_rgba(15,23,42,0.34)] dark:border-zinc-800 dark:bg-zinc-950">
|
</>
|
||||||
{highlightedItem.children.map((child) => (
|
) : null}
|
||||||
<DropdownMenuItem
|
</div>
|
||||||
className="rounded-xl px-3 py-2.5 font-medium text-slate-700 text-sm focus:bg-slate-50 dark:text-slate-100 dark:focus:bg-zinc-900"
|
);
|
||||||
key={child.id}
|
})()
|
||||||
onSelect={() =>
|
: null}
|
||||||
handleInsertWorkflowNodeFromMenu(child.templateType)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{child.label}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuSubContent>
|
|
||||||
</DropdownMenuSub>
|
|
||||||
) : (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="rounded-2xl px-4 py-3 font-semibold text-[15px] text-orange-600 focus:bg-orange-50 dark:text-orange-300 dark:focus:bg-orange-950/40"
|
|
||||||
onSelect={() =>
|
|
||||||
highlightedItem.templateType &&
|
|
||||||
handleInsertWorkflowNodeFromMenu(
|
|
||||||
highlightedItem.templateType
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<HighlightIcon className="h-5 w-5 text-orange-500" />
|
|
||||||
<span>{highlightedItem.label}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isVideoMode && isVideoModuleSidebarEnabled ? (
|
{isVideoMode && isVideoModuleSidebarEnabled ? (
|
||||||
<div
|
<div
|
||||||
@ -8930,7 +8970,9 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
<button
|
<button
|
||||||
aria-label="展开关联模块"
|
aria-label="展开关联模块"
|
||||||
className="-translate-x-1/2 -translate-y-1/2 pointer-events-auto absolute top-0 left-0 flex h-14 w-14 items-center justify-center rounded-[20px] border border-[#d7dff1] bg-white text-[#5b76fe] shadow-[0_0_0_1px_rgba(219,226,238,0.34),0_18px_34px_rgba(22,24,29,0.12)] transition-colors hover:bg-[#f5f8ff] hover:text-[#3954db] dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-300 dark:hover:bg-zinc-900 dark:hover:text-zinc-100"
|
className="-translate-x-1/2 -translate-y-1/2 pointer-events-auto absolute top-0 left-0 flex h-14 w-14 items-center justify-center rounded-[20px] border border-[#d7dff1] bg-white text-[#5b76fe] shadow-[0_0_0_1px_rgba(219,226,238,0.34),0_18px_34px_rgba(22,24,29,0.12)] transition-colors hover:bg-[#f5f8ff] hover:text-[#3954db] dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-300 dark:hover:bg-zinc-900 dark:hover:text-zinc-100"
|
||||||
onClick={() =>
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
setVideoCanvasInsertMenu({
|
setVideoCanvasInsertMenu({
|
||||||
screenPosition: {
|
screenPosition: {
|
||||||
x: selectedWorkflowQuickConnectAnchor.x,
|
x: selectedWorkflowQuickConnectAnchor.x,
|
||||||
@ -8944,8 +8986,10 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
selectedWorkflowQuickConnectSourceNode.position.y +
|
selectedWorkflowQuickConnectSourceNode.position.y +
|
||||||
selectedWorkflowQuickConnectSourceNode.size.height / 2,
|
selectedWorkflowQuickConnectSourceNode.size.height / 2,
|
||||||
},
|
},
|
||||||
})
|
sourceNodeId: selectedWorkflowQuickConnectSourceNode.id,
|
||||||
}
|
openLeft: selectedWorkflowQuickConnectAnchor.openLeft,
|
||||||
|
});
|
||||||
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Plus className="h-8 w-8" />
|
<Plus className="h-8 w-8" />
|
||||||
@ -8959,12 +9003,37 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
? -1
|
? -1
|
||||||
: 1;
|
: 1;
|
||||||
const Icon = getVideoWorkflowQuickConnectIcon(item.type);
|
const Icon = getVideoWorkflowQuickConnectIcon(item.type);
|
||||||
|
const definition = getVideoWorkflowNodeDefinition(item.type);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
aria-label={item.label}
|
aria-label={item.label}
|
||||||
className="-translate-x-1/2 -translate-y-1/2 pointer-events-auto absolute flex h-12 w-12 items-center justify-center rounded-[18px] border border-[#dbe2ee] bg-white text-[#566377] shadow-[0_0_0_1px_rgba(219,226,238,0.34),0_14px_26px_rgba(22,24,29,0.1)] transition-all hover:scale-[1.03] hover:border-[#cfdcff] hover:bg-[#f5f8ff] hover:text-[#3954db] dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-300 dark:hover:bg-zinc-900 dark:hover:text-zinc-100"
|
className="-translate-x-1/2 -translate-y-1/2 pointer-events-auto absolute flex h-12 w-12 items-center justify-center rounded-[18px] border border-[#dbe2ee] bg-white text-[#566377] shadow-[0_0_0_1px_rgba(219,226,238,0.34),0_14px_26px_rgba(22,24,29,0.1)] transition-all hover:scale-[1.03] hover:border-[#cfdcff] hover:bg-[#f5f8ff] hover:text-[#3954db] dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-300 dark:hover:bg-zinc-900 dark:hover:text-zinc-100"
|
||||||
key={`quick-connect-node-${item.type}`}
|
key={`quick-connect-node-${item.type}`}
|
||||||
onClick={() => handleAddWorkflowNode(item.type)}
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const position = definition
|
||||||
|
? {
|
||||||
|
x: selectedWorkflowQuickConnectAnchor.openLeft
|
||||||
|
? selectedWorkflowQuickConnectSourceNode.position.x -
|
||||||
|
definition.size.width -
|
||||||
|
180
|
||||||
|
: selectedWorkflowQuickConnectSourceNode.position.x +
|
||||||
|
selectedWorkflowQuickConnectSourceNode.size.width +
|
||||||
|
180,
|
||||||
|
y:
|
||||||
|
selectedWorkflowQuickConnectSourceNode.position.y +
|
||||||
|
selectedWorkflowQuickConnectSourceNode.size.height / 2 -
|
||||||
|
definition.size.height / 2 +
|
||||||
|
y / viewport.zoom,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
handleAddWorkflowNode(item.type, {
|
||||||
|
connectFromSelection: true,
|
||||||
|
connectFromNodeId: selectedWorkflowQuickConnectSourceNode.id,
|
||||||
|
...(position ? { position } : {}),
|
||||||
|
});
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
left: `${x * direction}px`,
|
left: `${x * direction}px`,
|
||||||
top: `${y}px`,
|
top: `${y}px`,
|
||||||
|
|||||||
@ -1380,14 +1380,14 @@ export function DrawnixWorkspace({
|
|||||||
src={displayLogo}
|
src={displayLogo}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-[#111827] font-bold text-white text-xs shadow-sm">
|
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-[#111827] font-bold text-white text-xs shadow-sm">
|
||||||
{siteName.slice(0, 1).toUpperCase()}
|
{siteName.slice(0, 1).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
aria-label="流程白板文件名"
|
aria-label="流程白板文件名"
|
||||||
className="h-7 max-w-[180px] rounded-lg border border-transparent bg-transparent px-2 font-bold text-[#111827] text-sm tracking-wide outline-none transition focus:border-black/10 focus:bg-[#f7f8fb]"
|
className="h-7 w-fit min-w-[120px] max-w-[200px] rounded-lg border border-transparent bg-transparent px-2 font-bold text-[#111827] text-sm tracking-wide outline-none transition focus:border-black/10 focus:bg-[#f7f8fb]"
|
||||||
onBlur={handleTitleCommit}
|
onBlur={handleTitleCommit}
|
||||||
onChange={(event) => setCurrentTitle(event.target.value)}
|
onChange={(event) => setCurrentTitle(event.target.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
@ -1397,7 +1397,7 @@ export function DrawnixWorkspace({
|
|||||||
}}
|
}}
|
||||||
value={currentTitle}
|
value={currentTitle}
|
||||||
/>
|
/>
|
||||||
<span className="rounded-md bg-[#f7f8fb] px-1.5 py-0.5 font-medium text-[#7a8297] text-[11px]">
|
<span className="shrink-0 rounded-md bg-[#f7f8fb] px-1.5 py-0.5 font-medium text-[#7a8297] text-[11px]">
|
||||||
{siteName}
|
{siteName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -1405,73 +1405,75 @@ export function DrawnixWorkspace({
|
|||||||
|
|
||||||
<div className="h-5 w-px bg-black/10" />
|
<div className="h-5 w-px bg-black/10" />
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex flex-1 items-center justify-between gap-2">
|
||||||
<button
|
<div className="flex items-center gap-1">
|
||||||
className="flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-medium text-[#4f566b] text-xs transition-colors hover:bg-black/5 hover:text-[#111827]"
|
<button
|
||||||
disabled={isSavingNow}
|
className="flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-medium text-[#4f566b] text-xs transition-colors hover:bg-black/5 hover:text-[#111827]"
|
||||||
onClick={() => flushPendingSave(true)}
|
disabled={isSavingNow}
|
||||||
type="button"
|
onClick={() => flushPendingSave(true)}
|
||||||
>
|
type="button"
|
||||||
{isSavingNow ? (
|
>
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
{isSavingNow ? (
|
||||||
) : (
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
<Save className="h-3.5 w-3.5" />
|
) : (
|
||||||
)}
|
<Save className="h-3.5 w-3.5" />
|
||||||
保存云端
|
)}
|
||||||
</button>
|
保存云端
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-medium text-[#4f566b] text-xs transition-colors hover:bg-black/5 hover:text-[#111827]"
|
className="flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-medium text-[#4f566b] text-xs transition-colors hover:bg-black/5 hover:text-[#111827]"
|
||||||
onClick={handleOpenFilePanel}
|
onClick={handleOpenFilePanel}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<FolderOpen className="h-3.5 w-3.5" />
|
<FolderOpen className="h-3.5 w-3.5" />
|
||||||
云文件
|
云文件
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-medium text-[#4f566b] text-xs transition-colors hover:bg-black/5 hover:text-[#111827] disabled:cursor-not-allowed disabled:opacity-50"
|
className="flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-medium text-[#4f566b] text-xs transition-colors hover:bg-black/5 hover:text-[#111827] disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
disabled={isSavingNow || isImportingMindMap}
|
disabled={isSavingNow || isImportingMindMap}
|
||||||
onClick={() => mindMapFileInputRef.current?.click()}
|
onClick={() => mindMapFileInputRef.current?.click()}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{isImportingMindMap ? (
|
{isImportingMindMap ? (
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Upload className="h-3.5 w-3.5" />
|
<Upload className="h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
导入
|
导入
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={`flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-medium text-xs transition-colors ${
|
className={`flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-medium text-xs transition-colors ${
|
||||||
isMarkerPanelOpen
|
isMarkerPanelOpen
|
||||||
? "bg-[#111827] text-white shadow-sm hover:bg-[#1f2937]"
|
? "bg-[#111827] text-white shadow-sm hover:bg-[#1f2937]"
|
||||||
: "text-[#4f566b] hover:bg-black/5 hover:text-[#111827]"
|
: "text-[#4f566b] hover:bg-black/5 hover:text-[#111827]"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsFilePanelOpen(false);
|
setIsFilePanelOpen(false);
|
||||||
setIsMarkerPanelOpen((open) => !open);
|
setIsMarkerPanelOpen((open) => !open);
|
||||||
refreshMarkerSelectionState();
|
refreshMarkerSelectionState();
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Tag className="h-3.5 w-3.5" />
|
<Tag className="h-3.5 w-3.5" />
|
||||||
标记
|
标记
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-medium text-[#4f566b] text-xs transition-colors hover:bg-black/5 hover:text-[#111827]"
|
className="flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-medium text-[#4f566b] text-xs transition-colors hover:bg-black/5 hover:text-[#111827]"
|
||||||
onClick={handleCreateCloudFile}
|
onClick={handleCreateCloudFile}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5" />
|
<Plus className="h-3.5 w-3.5" />
|
||||||
新建
|
新建
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="ml-1 flex items-center gap-1.5 px-2 text-[#7a8297]">
|
<div className="flex shrink-0 items-center gap-1.5 pl-2 text-[#7a8297]">
|
||||||
<Save className="h-3.5 w-3.5" />
|
<Save className="h-3.5 w-3.5" />
|
||||||
<span className="font-medium text-xs">
|
<span className="whitespace-nowrap font-medium text-xs">
|
||||||
{formatSavedAt(lastSavedAt)}
|
{formatSavedAt(lastSavedAt)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user