fix: stabilize video canvas quick connect
This commit is contained in:
parent
e054dcabcc
commit
3ad76b5c54
@ -120,9 +120,6 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@ -274,6 +271,8 @@ interface VideoCanvasProjectExportPayload {
|
||||
interface VideoCanvasInsertMenuState {
|
||||
screenPosition: Position;
|
||||
canvasPosition: Position;
|
||||
sourceNodeId?: string | null;
|
||||
openLeft?: boolean;
|
||||
}
|
||||
|
||||
interface VideoCanvasInsertMenuChild {
|
||||
@ -1430,6 +1429,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
});
|
||||
const [videoCanvasInsertMenu, setVideoCanvasInsertMenu] =
|
||||
useState<VideoCanvasInsertMenuState | null>(null);
|
||||
const videoCanvasInsertMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isVideoCanvasFileListOpen, setIsVideoCanvasFileListOpen] =
|
||||
useState(false);
|
||||
const [isVideoCanvasMarkerPanelOpen, setIsVideoCanvasMarkerPanelOpen] =
|
||||
@ -2708,6 +2708,31 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
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(() => {
|
||||
if (isAgentMode) {
|
||||
setIsPanelOpen(true);
|
||||
@ -5714,15 +5739,18 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
nodeConfig: Parameters<typeof addNode>[0],
|
||||
options?: {
|
||||
connectFromSelection?: boolean;
|
||||
connectFromNodeId?: string | null;
|
||||
selectNewNode?: boolean;
|
||||
}
|
||||
) => {
|
||||
const connectFromSelection = options?.connectFromSelection ?? true;
|
||||
const selectNewNode = options?.selectNewNode ?? true;
|
||||
const explicitSourceNodeId = options?.connectFromNodeId || null;
|
||||
const nodeId = addNode(nodeConfig);
|
||||
const sourceNodeId =
|
||||
connectFromSelection && isVideoMode && selectedNodeIds.length === 1
|
||||
? selectedNodeIds[0] || null
|
||||
connectFromSelection && isVideoMode
|
||||
? explicitSourceNodeId ||
|
||||
(selectedNodeIds.length === 1 ? selectedNodeIds[0] || null : null)
|
||||
: null;
|
||||
if (sourceNodeId && sourceNodeId !== nodeId) {
|
||||
addEdge({
|
||||
@ -5745,6 +5773,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
options?: {
|
||||
position?: Position;
|
||||
connectFromSelection?: boolean;
|
||||
connectFromNodeId?: string | null;
|
||||
selectNewNode?: boolean;
|
||||
}
|
||||
) => {
|
||||
@ -5754,6 +5783,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
options?.position || getVideoWorkflowPlacement(definition.size);
|
||||
createCanvasNode(buildVideoWorkflowCanvasNode(templateType, placement), {
|
||||
connectFromSelection: options?.connectFromSelection ?? true,
|
||||
connectFromNodeId: options?.connectFromNodeId ?? null,
|
||||
selectNewNode: options?.selectNewNode ?? true,
|
||||
});
|
||||
},
|
||||
@ -5773,7 +5803,21 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
if (!videoCanvasInsertMenu) return;
|
||||
|
||||
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:
|
||||
videoCanvasInsertMenu.canvasPosition.x -
|
||||
@ -5786,11 +5830,12 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
|
||||
handleAddWorkflowNode(templateType, {
|
||||
position: anchoredPosition,
|
||||
connectFromSelection: false,
|
||||
connectFromSelection: Boolean(sourceNode),
|
||||
connectFromNodeId: sourceNode?.id || null,
|
||||
});
|
||||
setVideoCanvasInsertMenu(null);
|
||||
},
|
||||
[handleAddWorkflowNode, videoCanvasInsertMenu]
|
||||
[handleAddWorkflowNode, nodes, videoCanvasInsertMenu]
|
||||
);
|
||||
|
||||
const handleCreateSelectedAgentResultBranches = useCallback(async () => {
|
||||
@ -8574,125 +8619,120 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isVideoMode && videoCanvasInsertMenu ? (
|
||||
<DropdownMenu
|
||||
modal={false}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setVideoCanvasInsertMenu(null);
|
||||
}
|
||||
}}
|
||||
open={Boolean(videoCanvasInsertMenu)}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none fixed h-px w-px opacity-0"
|
||||
style={{
|
||||
left: `${videoCanvasInsertMenu.screenPosition.x}px`,
|
||||
top: `${videoCanvasInsertMenu.screenPosition.y}px`,
|
||||
}}
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
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"
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
sideOffset={14}
|
||||
>
|
||||
<div className="px-4 py-3 font-semibold text-[14px] text-slate-500 dark:text-zinc-400">
|
||||
@{siteSettingsData?.siteName || "视频画布"}
|
||||
</div>
|
||||
|
||||
{VIDEO_CANVAS_INSERT_MENU_ITEMS.slice(0, -1).map((item) => {
|
||||
{isVideoMode && videoCanvasInsertMenu
|
||||
? (() => {
|
||||
const menuWidth = 312;
|
||||
const menuLeft =
|
||||
viewportWidth > 0
|
||||
? Math.max(
|
||||
12,
|
||||
Math.min(
|
||||
videoCanvasInsertMenu.screenPosition.x,
|
||||
Math.max(12, viewportWidth - menuWidth - 12)
|
||||
)
|
||||
)
|
||||
: videoCanvasInsertMenu.screenPosition.x;
|
||||
const menuTop = Math.max(
|
||||
12,
|
||||
videoCanvasInsertMenu.screenPosition.y + 14
|
||||
);
|
||||
const renderChildButton = (child: VideoCanvasInsertMenuChild) => (
|
||||
<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}
|
||||
onClick={() =>
|
||||
handleInsertWorkflowNodeFromMenu(child.templateType)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
{child.label}
|
||||
</button>
|
||||
);
|
||||
const renderInsertItem = (
|
||||
item: VideoCanvasInsertMenuItem,
|
||||
highlighted = false
|
||||
) => {
|
||||
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) {
|
||||
return (
|
||||
<DropdownMenuSub 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">
|
||||
<Icon className="h-5 w-5 text-slate-400 dark:text-zinc-500" />
|
||||
<span>{item.label}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<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
|
||||
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={() =>
|
||||
handleInsertWorkflowNodeFromMenu(
|
||||
child.templateType
|
||||
)
|
||||
}
|
||||
>
|
||||
{child.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<div className="rounded-2xl" key={item.id}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-2xl px-4 py-3 font-semibold text-[15px]",
|
||||
textClassName
|
||||
)}
|
||||
>
|
||||
<Icon className={cn("h-5 w-5", iconClassName)} />
|
||||
<span className="flex-1">{item.label}</span>
|
||||
<ChevronRight className="h-4 w-4 opacity-60" />
|
||||
</div>
|
||||
<div className="ml-8 mr-2 mb-1 grid gap-0.5 border-slate-100 border-l pl-2 dark:border-zinc-800">
|
||||
{item.children.map(renderChildButton)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
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"
|
||||
<button
|
||||
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}
|
||||
onSelect={() =>
|
||||
onClick={() =>
|
||||
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>
|
||||
</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>
|
||||
|
||||
{(() => {
|
||||
const highlightedItem = VIDEO_CANVAS_INSERT_MENU_ITEMS.at(-1);
|
||||
if (!highlightedItem) return null;
|
||||
const HighlightIcon = highlightedItem.icon;
|
||||
return highlightedItem.children?.length ? (
|
||||
<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">
|
||||
<HighlightIcon className="h-5 w-5 text-orange-500" />
|
||||
<span>{highlightedItem.label}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<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) => (
|
||||
<DropdownMenuItem
|
||||
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={() =>
|
||||
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}
|
||||
<div className="grid gap-0.5">
|
||||
{VIDEO_CANVAS_INSERT_MENU_ITEMS.slice(0, -1).map((item) =>
|
||||
renderInsertItem(item)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{highlightedItem ? (
|
||||
<>
|
||||
<div className="mx-3 my-2 h-px bg-slate-200 dark:bg-zinc-800" />
|
||||
{renderInsertItem(highlightedItem, true)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
: null}
|
||||
|
||||
{isVideoMode && isVideoModuleSidebarEnabled ? (
|
||||
<div
|
||||
@ -8930,7 +8970,9 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
<button
|
||||
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"
|
||||
onClick={() =>
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setVideoCanvasInsertMenu({
|
||||
screenPosition: {
|
||||
x: selectedWorkflowQuickConnectAnchor.x,
|
||||
@ -8944,8 +8986,10 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
selectedWorkflowQuickConnectSourceNode.position.y +
|
||||
selectedWorkflowQuickConnectSourceNode.size.height / 2,
|
||||
},
|
||||
})
|
||||
}
|
||||
sourceNodeId: selectedWorkflowQuickConnectSourceNode.id,
|
||||
openLeft: selectedWorkflowQuickConnectAnchor.openLeft,
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Plus className="h-8 w-8" />
|
||||
@ -8959,12 +9003,37 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
? -1
|
||||
: 1;
|
||||
const Icon = getVideoWorkflowQuickConnectIcon(item.type);
|
||||
const definition = getVideoWorkflowNodeDefinition(item.type);
|
||||
return (
|
||||
<button
|
||||
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"
|
||||
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={{
|
||||
left: `${x * direction}px`,
|
||||
top: `${y}px`,
|
||||
|
||||
@ -1380,14 +1380,14 @@ export function DrawnixWorkspace({
|
||||
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()}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
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}
|
||||
onChange={(event) => setCurrentTitle(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
@ -1397,7 +1397,7 @@ export function DrawnixWorkspace({
|
||||
}}
|
||||
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}
|
||||
</span>
|
||||
</div>
|
||||
@ -1405,73 +1405,75 @@ export function DrawnixWorkspace({
|
||||
|
||||
<div className="h-5 w-px bg-black/10" />
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<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={isSavingNow}
|
||||
onClick={() => flushPendingSave(true)}
|
||||
type="button"
|
||||
>
|
||||
{isSavingNow ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-3.5 w-3.5" />
|
||||
)}
|
||||
保存云端
|
||||
</button>
|
||||
<div className="flex flex-1 items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<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={isSavingNow}
|
||||
onClick={() => flushPendingSave(true)}
|
||||
type="button"
|
||||
>
|
||||
{isSavingNow ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-3.5 w-3.5" />
|
||||
)}
|
||||
保存云端
|
||||
</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]"
|
||||
onClick={handleOpenFilePanel}
|
||||
type="button"
|
||||
>
|
||||
<FolderOpen className="h-3.5 w-3.5" />
|
||||
云文件
|
||||
</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]"
|
||||
onClick={handleOpenFilePanel}
|
||||
type="button"
|
||||
>
|
||||
<FolderOpen className="h-3.5 w-3.5" />
|
||||
云文件
|
||||
</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"
|
||||
disabled={isSavingNow || isImportingMindMap}
|
||||
onClick={() => mindMapFileInputRef.current?.click()}
|
||||
type="button"
|
||||
>
|
||||
{isImportingMindMap ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-3.5 w-3.5" />
|
||||
)}
|
||||
导入
|
||||
</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"
|
||||
disabled={isSavingNow || isImportingMindMap}
|
||||
onClick={() => mindMapFileInputRef.current?.click()}
|
||||
type="button"
|
||||
>
|
||||
{isImportingMindMap ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-3.5 w-3.5" />
|
||||
)}
|
||||
导入
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-medium text-xs transition-colors ${
|
||||
isMarkerPanelOpen
|
||||
? "bg-[#111827] text-white shadow-sm hover:bg-[#1f2937]"
|
||||
: "text-[#4f566b] hover:bg-black/5 hover:text-[#111827]"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setIsFilePanelOpen(false);
|
||||
setIsMarkerPanelOpen((open) => !open);
|
||||
refreshMarkerSelectionState();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Tag className="h-3.5 w-3.5" />
|
||||
标记
|
||||
</button>
|
||||
<button
|
||||
className={`flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-medium text-xs transition-colors ${
|
||||
isMarkerPanelOpen
|
||||
? "bg-[#111827] text-white shadow-sm hover:bg-[#1f2937]"
|
||||
: "text-[#4f566b] hover:bg-black/5 hover:text-[#111827]"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setIsFilePanelOpen(false);
|
||||
setIsMarkerPanelOpen((open) => !open);
|
||||
refreshMarkerSelectionState();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Tag className="h-3.5 w-3.5" />
|
||||
标记
|
||||
</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]"
|
||||
onClick={handleCreateCloudFile}
|
||||
type="button"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
新建
|
||||
</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]"
|
||||
onClick={handleCreateCloudFile}
|
||||
type="button"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
新建
|
||||
</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" />
|
||||
<span className="font-medium text-xs">
|
||||
<span className="whitespace-nowrap font-medium text-xs">
|
||||
{formatSavedAt(lastSavedAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user