@@ -20,8 +20,12 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
|
||||
|
||||
画布图片/视频节点“放大编辑”窗口改为 `min(1200px, 92vw)`,编辑区最小高度 200px,右下角可对角拖拽,方向键可微调。不改变节点浮动面板的现有视觉。
|
||||
|
||||
- [ ] 点击放大后窗口明显大于节点面板;右下角拖动手柄可同时改宽高,最小约 560×320,不超出视口。
|
||||
PR #507 核心能力已按主分支实现保留,并补齐小视口尺寸约束、浏览器缩放窗口后的尺寸收敛、短屏滚动和展开态单一调整手柄。尺寸回归测试与 Vite 生产打包通过;真实 Chrome 组件验证(临时数据,未调用生成接口)覆盖图片/视频、参考图、首尾帧控件、明暗主题、窄屏/短屏、鼠标与键盘调整、切换节点重置。完整 `bun run build` 仍被既有 `canvas-cloud-agent-panel.tsx` 中未定义的 `markdownPlainText` 阻断。
|
||||
|
||||
- [ ] 点击放大后窗口明显大于节点面板;右下角拖动手柄可同时改宽高,桌面最小约 560×320,小视口优先限制在视口减 24px 内。
|
||||
- [ ] 方向键可调整,Shift 加速;切换节点后尺寸恢复默认。
|
||||
- [ ] 窗口缩小后弹窗尺寸自动收敛;短屏下可滚动访问编辑区、参数和提交按钮,右下角手柄保持可达。
|
||||
- [ ] 展开态仅显示右下角窗口手柄;紧凑面板仍可独立调整输入高度。明暗主题继续跟随当前画布。
|
||||
|
||||
## 支付订单与对账 CSV 导出
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
12
web/src/lib/canvas/canvas-prompt-editor-size.ts
Normal file
12
web/src/lib/canvas/canvas-prompt-editor-size.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
type Size = { width: number; height: number };
|
||||
|
||||
export const PROMPT_EDITOR_VIEWPORT_MARGIN = 24;
|
||||
|
||||
export function clampPromptEditorModalSize(size: Size, viewport: Size): Size {
|
||||
const maxWidth = Math.max(1, viewport.width - PROMPT_EDITOR_VIEWPORT_MARGIN);
|
||||
const maxHeight = Math.max(1, viewport.height - PROMPT_EDITOR_VIEWPORT_MARGIN);
|
||||
return {
|
||||
width: Math.min(maxWidth, Math.max(560, Math.round(size.width))),
|
||||
height: Math.min(maxHeight, Math.max(320, Math.round(size.height))),
|
||||
};
|
||||
}
|
||||
22
web/test/canvas-prompt-editor-size.test.ts
Normal file
22
web/test/canvas-prompt-editor-size.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { clampPromptEditorModalSize } from "../src/lib/canvas/canvas-prompt-editor-size";
|
||||
|
||||
describe("prompt editor window bounds", () => {
|
||||
test("keeps desktop minimums and leaves a viewport margin", () => {
|
||||
const viewport = { width: 1440, height: 900 };
|
||||
expect(clampPromptEditorModalSize({ width: 100, height: 100 }, viewport)).toEqual({ width: 560, height: 320 });
|
||||
expect(clampPromptEditorModalSize({ width: 2000, height: 2000 }, viewport)).toEqual({ width: 1416, height: 876 });
|
||||
});
|
||||
|
||||
test("a narrow or short viewport takes precedence over desktop minimums", () => {
|
||||
expect(clampPromptEditorModalSize({ width: 1200, height: 800 }, { width: 390, height: 844 })).toEqual({ width: 366, height: 800 });
|
||||
expect(clampPromptEditorModalSize({ width: 1200, height: 800 }, { width: 844, height: 300 })).toEqual({ width: 820, height: 276 });
|
||||
});
|
||||
|
||||
test("reconstrains a previously resized window after viewport shrink", () => {
|
||||
const desktop = clampPromptEditorModalSize({ width: 1200, height: 800 }, { width: 1440, height: 900 });
|
||||
const mobile = clampPromptEditorModalSize(desktop, { width: 390, height: 300 });
|
||||
expect(mobile).toEqual({ width: 366, height: 276 });
|
||||
expect(clampPromptEditorModalSize(mobile, { width: 390, height: 300 })).toEqual(mobile);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user