fix(canvas): 提示词编辑 - 修复小视口溢出与窗口调整手柄失效

Refs #507
This commit is contained in:
ddCat
2026-09-18 13:19:57 +08:00
parent 10f2aa302c
commit 4eaa28b807
4 changed files with 72 additions and 27 deletions

View File

@@ -6,6 +6,7 @@ import { ArrowLeftRight, ArrowUp, AtSign, Boxes, Camera, ChevronDown, FileText,
import { ModelPicker } from "@/components/model-picker";
import { defaultConfig, modelOptionName, resolveModelChannel, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store";
import { resolveCanvasGenerationModel } from "@/lib/canvas/canvas-project-generation";
import { clampPromptEditorModalSize, PROMPT_EDITOR_VIEWPORT_MARGIN } from "@/lib/canvas/canvas-prompt-editor-size";
import { CreditSymbol, requestCreditCost } from "@/constant/credits";
import { canvasThemes } from "@/lib/canvas-theme";
import { modelQuoteRequest } from "@/lib/model-pricing";
@@ -67,9 +68,6 @@ const PROMPT_EDITOR_EXPANDED_MAX_LINES = 14;
const PROMPT_EDITOR_MODAL_WIDTH = "min(1200px, 92vw)";
const PROMPT_EDITOR_MODAL_DEFAULT_WIDTH = 1200;
const PROMPT_EDITOR_MODAL_DEFAULT_HEIGHT = 420;
const PROMPT_EDITOR_MODAL_MIN_WIDTH = 560;
const PROMPT_EDITOR_MODAL_MIN_HEIGHT = 320;
const PROMPT_EDITOR_MODAL_VIEWPORT_MARGIN = 24;
export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChange, onConfigChange, onGenerate, mentionReferences = [], onAddReference, onRemoveReference, onReorderReferences, onReplaceReference, onReplaceReferenceFiles, onClose, onNodeMouseDown, onImageSettingsOpenChange, workspaceMode = "professional" }: CanvasNodePromptPanelProps) {
const globalConfig = useEffectiveConfig();
@@ -92,7 +90,6 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
const [promptContentHeight, setPromptContentHeight] = useState(() => estimatePromptContentHeight(savedPrompt, false));
const [expandedPromptContentHeight, setExpandedPromptContentHeight] = useState(() => estimatePromptContentHeight(savedPrompt, true));
const [manualPromptHeight, setManualPromptHeight] = useState<number | null>(null);
const [manualExpandedPromptHeight, setManualExpandedPromptHeight] = useState<number | null>(null);
const [paramsExpanded, setParamsExpanded] = useState(false); // #98 决策2:B区参数区折叠状态(手风琴)
const [promptOptimizerOpen, setPromptOptimizerOpen] = useState(false);
const [autoLinkEnabled, setAutoLinkEnabled] = useState(true);
@@ -172,7 +169,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
const promptBounds = promptEditorBounds(false, activeReferenceCount > 0);
const expandedPromptBounds = promptEditorBounds(true, activeReferenceCount > 0);
const composerHeight = clampPromptHeight(manualPromptHeight ?? promptContentHeight + (activeReferenceCount ? PROMPT_REFERENCE_SHELF_HEIGHT : 0), promptBounds);
const expandedComposerHeight = clampPromptHeight(manualExpandedPromptHeight ?? expandedPromptContentHeight + (activeReferenceCount ? PROMPT_REFERENCE_SHELF_HEIGHT : 0), expandedPromptBounds);
const expandedComposerHeight = clampPromptHeight(expandedPromptContentHeight + (activeReferenceCount ? PROMPT_REFERENCE_SHELF_HEIGHT : 0), expandedPromptBounds);
const measureExpandedModalSize = () => {
const rect = expandedModalRef.current?.getBoundingClientRect();
if (!rect?.width || !rect.height) return { width: PROMPT_EDITOR_MODAL_DEFAULT_WIDTH, height: PROMPT_EDITOR_MODAL_DEFAULT_HEIGHT };
@@ -197,9 +194,20 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
setPromptContentHeight(estimatePromptContentHeight(normalizedSavedPrompt, false));
setExpandedPromptContentHeight(estimatePromptContentHeight(normalizedSavedPrompt, true));
setManualPromptHeight(null);
setManualExpandedPromptHeight(null);
}, [node.id]);
useEffect(() => {
if (!expandedPromptOpen) return;
const constrainSize = () => setExpandedModalSize((size) => {
if (!size) return size;
const next = clampExpandedModalSize(size);
return next.width === size.width && next.height === size.height ? size : next;
});
constrainSize();
window.addEventListener("resize", constrainSize);
return () => window.removeEventListener("resize", constrainSize);
}, [expandedPromptOpen]);
useEffect(() => {
if (!creditsEnabled || !quoteRequest) {
setQuotedCredits(null);
@@ -455,7 +463,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
const height = expanded ? expandedComposerHeight : composerHeight;
return (
<>
<div className={fill ? "canvas-node-composer-editor min-h-0 flex-1" : "canvas-node-composer-editor"} style={fill ? undefined : { height }}>
<div className={fill ? "canvas-node-composer-editor flex-1" : "canvas-node-composer-editor"} style={fill ? { minHeight: bounds.min } : { height, ...(expanded ? { flexShrink: 0 } : null) }}>
<ConnectedReferenceShelf
targetNodeId={node.id}
references={resolvedMentionReferences}
@@ -484,12 +492,12 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
aria-label={`${modeDisplayName(mode)}提示词`}
/>
</div>
<PromptResizeHandle
{!expanded && <PromptResizeHandle
height={height}
min={bounds.min}
max={bounds.max}
onResize={expanded ? setManualExpandedPromptHeight : setManualPromptHeight}
/>
onResize={setManualPromptHeight}
/>}
</>
);
};
@@ -551,6 +559,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
footer={null}
centered
width={expandedModalSize ? expandedModalSize.width : PROMPT_EDITOR_MODAL_WIDTH}
style={{ maxWidth: `calc(100vw - ${PROMPT_EDITOR_VIEWPORT_MARGIN}px)` }}
destroyOnHidden
onCancel={() => {
setExpandedPresetOpen(false);
@@ -561,15 +570,17 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
body: { minHeight: 0, padding: 0 },
}}
>
<div ref={expandedModalRef} className="relative flex min-h-0 flex-col gap-2.5 overflow-visible p-3" style={{ ...composerTokens, color: theme.node.text, ...(expandedModalSize ? { height: expandedModalSize.height } : null) }}>
<div className="shrink-0 pr-8">{renderComposerHeader(true)}</div>
{renderPromptEditor(true, Boolean(expandedModalSize))}
{hasVideoPromptTools ? (
<div className="canvas-node-composer-parameters shrink-0">
<CanvasVideoPromptTools metadata={node.metadata} frameOptions={videoFrameOptions} onMetadataChange={(patch) => onConfigChange(node.id, patch)} />
</div>
) : null}
<div className="shrink-0">{renderComposerControls(true)}</div>
<div ref={expandedModalRef} className="relative flex min-h-0 flex-col" style={{ ...composerTokens, color: theme.node.text, maxHeight: `calc(100dvh - ${PROMPT_EDITOR_VIEWPORT_MARGIN}px)`, ...(expandedModalSize ? { height: expandedModalSize.height } : null) }}>
<div className="flex min-h-0 flex-1 flex-col gap-2.5 overflow-y-auto p-3">
<div className="shrink-0 pr-8">{renderComposerHeader(true)}</div>
{renderPromptEditor(true, Boolean(expandedModalSize))}
{hasVideoPromptTools ? (
<div className="canvas-node-composer-parameters shrink-0">
<CanvasVideoPromptTools metadata={node.metadata} frameOptions={videoFrameOptions} onMetadataChange={(patch) => onConfigChange(node.id, patch)} />
</div>
) : null}
<div className="shrink-0">{renderComposerControls(true)}</div>
</div>
<PromptModalResizeHandle size={expandedModalSize} measure={measureExpandedModalSize} onResize={setExpandedModalSize} accent={theme.node.muted} />
</div>
</Modal>
@@ -933,12 +944,7 @@ function PromptResizeHandle({ height, min, max, onResize }: { height: number; mi
}
function clampExpandedModalSize(size: { width: number; height: number }) {
const maxWidth = Math.max(PROMPT_EDITOR_MODAL_MIN_WIDTH, window.innerWidth - PROMPT_EDITOR_MODAL_VIEWPORT_MARGIN);
const maxHeight = Math.max(PROMPT_EDITOR_MODAL_MIN_HEIGHT, window.innerHeight - PROMPT_EDITOR_MODAL_VIEWPORT_MARGIN);
return {
width: Math.min(maxWidth, Math.max(PROMPT_EDITOR_MODAL_MIN_WIDTH, Math.round(size.width))),
height: Math.min(maxHeight, Math.max(PROMPT_EDITOR_MODAL_MIN_HEIGHT, Math.round(size.height))),
};
return clampPromptEditorModalSize(size, { width: window.innerWidth, height: window.innerHeight });
}
function PromptModalResizeHandle({ size, measure, onResize, accent }: { size: { width: number; height: number } | null; measure: () => { width: number; height: number }; onResize: (size: { width: number; height: number }) => void; accent: string }) {
@@ -989,7 +995,8 @@ function PromptModalResizeHandle({ size, measure, onResize, accent }: { size: {
return;
}
event.stopPropagation();
onResize(clampExpandedModalSize({ width: drag.width + event.clientX - drag.startX, height: drag.height + event.clientY - drag.startY }));
// The modal stays centered, so each edge moves by half the size change.
onResize(clampExpandedModalSize({ width: drag.width + 2 * (event.clientX - drag.startX), height: drag.height + 2 * (event.clientY - drag.startY) }));
}}
onPointerUp={finishResize}
onPointerCancel={finishResize}