fix(canvas): 分镜脚本节点文字溢出与视频模型下拉异常 #84 (#85)

* fix(canvas): 分镜脚本节点文字溢出与视频模型下拉异常 #84

- CompactInput 补齐 overflow-y-auto/whitespace-pre-wrap/break-words,长文本不再横向溢出撑开列宽
- filterModelsByCapability 增加 modelCosts[].protocol 协议层回退,未显式配置 capability 时通过协议识别能力
- isVideoModelName 扩展 pika/runway/gen-3/hunyuan-video 等主流视频模型名关键字
- ModelPicker.options 保留当前选中 value,避免过滤后 options 为空触发跳转设置页

* fix(canvas): 模型过滤协议反向排除 + 分镜脚本节点模型选择提示 #84

- filterModelsByCapability 协议层提升至最高优先级,协议明确属于其他能力时直接排除,
  防止 video/image/audio 协议的模型被误标为 text 后混入文本下拉
- isVideoModelName 补充 cogvideo/mochi/latte/stable-video/svd/animatediff/ltx-video/minimax-video/abab-video 等关键字
- isImageModelName 补充 nano-banana/ideogram/recraft/playground/leonardo 等关键字
- 分镜脚本节点 ModelPicker 包裹 Tooltip,提示"脚本生成仅支持文本模型"

* fix(canvas): 节点标题截断修复 — flex min-width 约束 + 宽度边界 + hover tooltip

- NodeExternalHeader: 容器增加 max-w-full 双重约束,窄节点时不越界,宽节点时上限 240px
- NodeExternalHeader: 标题 span 增加 min-w-0,解决 flex min-width:auto 导致 truncate 失效
- 所有节点标题(绘图/技能/角色参考/音频/分镜脚本)增加 title 属性,hover 显示完整标题
- AudioNodeContent: 标题 span 增加 min-w-0,修复 flex 子项截断

* fix(canvas): 节点顶部标题真正截断 — JS 计算 maxWidth + overflow-hidden + flex-1

- NodeExternalHeader 容器: 移除 Tailwind 级联冲突的 max-w-[240px]/max-w-full,
  改用 JS 计算 Math.min(240, node.width) 作为 inline style maxWidth,
  从根本上解决两个 max-width 声明的优先级不确定问题
- 容器增加 overflow-hidden 作为第二道防线
- 内部 text/input/button 元素改用 flex-1 + min-w-0 替代固定 max-w-[Npx],
  让 flex 子项自动填充可用空间,文本在剩余空间内截断

* fix(canvas): 节点标题缩放溢出 — 考虑 canvas zoom scale

- maxHeaderWidth 改为 Math.min(240, node.width * scale),
  因为 header 的视觉宽度 = maxHeaderWidth(inverseScale 与 canvas scale 抵消),
  而节点的视觉宽度 = node.width * scale,两者必须比较同一维度
- 修复 zoom 缩小时(scale < 1)header 视觉宽度超过节点视觉宽度的溢出问题

---------

Co-authored-by: sikongyue <3155157671@qq.com>
This commit is contained in:
爱笑的毛毛虫
2026-08-03 08:40:04 +08:00
committed by GitHub
parent 9c910e8823
commit f8cb3d0201
4 changed files with 77 additions and 31 deletions

View File

@@ -126,7 +126,7 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
<div className="relative flex h-full w-full flex-col overflow-visible" style={{ color: theme.node.text }} onDoubleClick={(event) => event.stopPropagation()}>
<div className="relative flex h-10 shrink-0 items-center gap-2 rounded-t-[17px] border-b px-4" style={{ borderColor: theme.node.stroke, background: theme.node.panel }}>
<Clapperboard className="size-4" />
<span className="min-w-0 flex-1 truncate text-sm font-semibold">{node.title || "分镜脚本"}</span>
<span className="min-w-0 flex-1 truncate text-sm font-semibold" title={node.title || "分镜脚本"}>{node.title || "分镜脚本"}</span>
{batchSummary ? <span className="min-w-0 max-w-[42%] truncate text-[11px] font-medium" title={batchSummary} style={{ color: batch?.status === "partial_failed" ? theme.accent.danger : theme.node.muted }}>{batchSummary}</span> : taskFeedback ? <span className="min-w-0 max-w-[38%] truncate text-[11px] font-medium" title={taskFeedback} style={{ color: node.metadata?.status === "error" ? theme.accent.danger : theme.node.muted }}>{taskFeedback}</span> : null}
<span className="text-xs font-medium" style={{ color: theme.node.muted }}>{rows.length} 镜 · {totalDuration}s</span>
{batch ? <>
@@ -210,19 +210,21 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
onWheel={(event) => event.stopPropagation()}
/>
<div className="flex min-w-0 items-center justify-end gap-2" onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
<div className="mr-auto min-w-36 max-w-56 flex-1">
<ModelPicker
className="!h-7 !w-full !min-w-0 !text-[10px] !font-normal [&_img]:!size-3 [&_.lucide]:!size-3"
fullWidth
config={generationConfig}
value={generationConfig.model}
capability="text"
placeholder="选择文本模型"
showSelectedPrice={false}
onChange={onModelChange}
onMissingConfig={() => navigateToSettings({ continueCreation: true })}
/>
</div>
<Tooltip title="脚本生成需要文本理解与结构化输出能力,仅展示文本模型;视频/图片模型无法生成分镜表" placement="topLeft">
<div className="mr-auto min-w-36 max-w-56 flex-1">
<ModelPicker
className="!h-7 !w-full !min-w-0 !text-[10px] !font-normal [&_img]:!size-3 [&_.lucide]:!size-3"
fullWidth
config={generationConfig}
value={generationConfig.model}
capability="text"
placeholder="选择文本模型"
showSelectedPrice={false}
onChange={onModelChange}
onMissingConfig={() => navigateToSettings({ continueCreation: true })}
/>
</div>
</Tooltip>
{simpleMode ? <span className="text-[11px]" style={{ color: theme.node.muted }}>自动拆分 · 默认时长</span> : <Select<StoryboardShotCount>
className="min-w-32"
size="small"
@@ -448,7 +450,7 @@ export function CanvasScriptEditor({ node, open, onClose, onUpdateRows, onVisibl
}
function CompactInput({ value, placeholder, borderColor, onChange }: { value: string; placeholder: string; borderColor: string; onChange: (value: string) => void }) {
return <textarea className="h-full resize-none border-r bg-transparent px-4 py-2.5 text-xs leading-5 outline-none transition placeholder:opacity-35 focus:bg-black/[0.02] dark:focus:bg-white/[0.025]" style={{ borderColor }} value={value} placeholder={placeholder} onChange={(event) => onChange(event.target.value)} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} />;
return <textarea className="thin-scrollbar h-full w-full resize-none overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-words border-r bg-transparent px-4 py-2.5 text-xs leading-5 outline-none transition placeholder:opacity-35 focus:bg-black/[0.02] dark:focus:bg-white/[0.025]" style={{ borderColor }} value={value} placeholder={placeholder} onChange={(event) => onChange(event.target.value)} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} />;
}
function HeaderCell({ children, borderColor, align = "left" }: { children: ReactNode; borderColor: string; align?: "left" | "center" }) {