refactor(core): 模型路由与画布 - 收敛渠道模型映射并优化交互

This commit is contained in:
ddCat
2026-08-19 20:27:26 +08:00
parent 52424543ea
commit d47975f9bc
34 changed files with 601 additions and 901 deletions

View File

@@ -22,7 +22,6 @@ type CanvasNodeProps = {
isRelated: boolean;
isFocusRelated: boolean;
isConnectionTarget: boolean;
isConnecting: boolean;
forceInputVisible?: boolean;
showImageInfo: boolean;
reduceMediaEffects?: boolean;
@@ -364,9 +363,9 @@ export const CanvasNode = React.memo(function CanvasNode({
/>
</div>
{hasReplaceAction ? (
{hasReplaceAction && (!hasMediaContent || isSelected || hovered) ? (
<div
className={`absolute right-3 top-3 z-[var(--node-z-overlay)] motion-safe:transition motion-safe:duration-200 ${!hasMediaContent || isSelected || hovered ? "opacity-100" : "pointer-events-none opacity-0"}`}
className="absolute right-3 top-3 z-[var(--node-z-overlay)]"
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
>
@@ -436,7 +435,7 @@ export const CanvasNode = React.memo(function CanvasNode({
</div>
) : null}
{!readOnly && !data.metadata?.locked ? <>
{!readOnly && !data.metadata?.locked && (isSelected || hovered) ? <>
<ResizeHandle corner="top-left" onMouseDown={handleResizeMouseDown} />
<ResizeHandle corner="top-right" onMouseDown={handleResizeMouseDown} />
<ResizeHandle corner="bottom-left" onMouseDown={handleResizeMouseDown} />
@@ -444,12 +443,61 @@ export const CanvasNode = React.memo(function CanvasNode({
</> : null}
</div>
{!readOnly && data.type !== CanvasNodeType.Script ? <ConnectionSideRail side="left" scale={scale} visible={hovered || forceInputVisible} theme={theme} onPointerDown={(event, anchorRatio) => onConnectStart(event, data.id, "target", undefined, anchorRatio)} /> : null}
{!readOnly && data.type !== CanvasNodeType.Script && data.type !== CanvasNodeType.Config ? <ConnectionSideRail side="right" scale={scale} visible={hovered} theme={theme} onPointerDown={(event, anchorRatio) => onConnectStart(event, data.id, "source", undefined, anchorRatio)} /> : null}
{!readOnly && data.type !== CanvasNodeType.Script && (hovered || forceInputVisible) ? <ConnectionSideRail side="left" scale={scale} theme={theme} onPointerDown={(event, anchorRatio) => onConnectStart(event, data.id, "target", undefined, anchorRatio)} /> : null}
{!readOnly && data.type !== CanvasNodeType.Script && data.type !== CanvasNodeType.Config && hovered ? <ConnectionSideRail side="right" scale={scale} theme={theme} onPointerDown={(event, anchorRatio) => onConnectStart(event, data.id, "source", undefined, anchorRatio)} /> : null}
</div>
);
});
}, areCanvasNodePropsEqual);
function areCanvasNodePropsEqual(previous: CanvasNodeProps, next: CanvasNodeProps) {
return (
previous.data === next.data &&
previous.dragOffset?.x === next.dragOffset?.x &&
previous.dragOffset?.y === next.dragOffset?.y &&
previous.scale === next.scale &&
previous.isSelected === next.isSelected &&
previous.isRelated === next.isRelated &&
previous.isFocusRelated === next.isFocusRelated &&
previous.isConnectionTarget === next.isConnectionTarget &&
previous.forceInputVisible === next.forceInputVisible &&
previous.showImageInfo === next.showImageInfo &&
previous.reduceMediaEffects === next.reduceMediaEffects &&
previous.readOnly === next.readOnly &&
previous.resourceLabel === next.resourceLabel &&
previous.mentionReferences === next.mentionReferences &&
previous.renderNodeContent === next.renderNodeContent &&
previous.drawingProjectId === next.drawingProjectId &&
previous.batchCount === next.batchCount &&
previous.batchExpanded === next.batchExpanded &&
previous.batchClosing === next.batchClosing &&
previous.batchOpening === next.batchOpening &&
previous.batchRecovering === next.batchRecovering &&
previous.batchPrimary === next.batchPrimary &&
previous.batchMotion?.x === next.batchMotion?.x &&
previous.batchMotion?.y === next.batchMotion?.y &&
previous.batchMotion?.index === next.batchMotion?.index &&
previous.onMouseDown === next.onMouseDown &&
previous.onHoverStart === next.onHoverStart &&
previous.onHoverEnd === next.onHoverEnd &&
previous.onConnectStart === next.onConnectStart &&
previous.onResize === next.onResize &&
previous.onTitleChange === next.onTitleChange &&
previous.onContentChange === next.onContentChange &&
previous.onToggleBatch === next.onToggleBatch &&
previous.onSetBatchPrimary === next.onSetBatchPrimary &&
previous.onRetry === next.onRetry &&
previous.onCancelTask === next.onCancelTask &&
previous.onOpenTaskDetails === next.onOpenTaskDetails &&
previous.onOpenVersions === next.onOpenVersions &&
previous.onViewImage === next.onViewImage &&
previous.onReplaceMedia === next.onReplaceMedia &&
previous.onOpenTextEditor === next.onOpenTextEditor &&
previous.onOpenDirector === next.onOpenDirector &&
previous.onOpenDrawing === next.onOpenDrawing &&
previous.onContextMenu === next.onContextMenu
);
}
function ResourceLabelBadge({ reference, theme }: { reference: CanvasResourceReference; theme: CanvasTheme }) {
return (
@@ -624,7 +672,7 @@ function NodeStatusBadge({ status }: { status: "loading" | "success" | "error" }
);
}
function ConnectionSideRail({ side, scale, visible, theme, onPointerDown }: { side: "left" | "right"; scale: number; visible: boolean; theme: CanvasTheme; onPointerDown: (event: React.PointerEvent, anchorRatio: number) => void }) {
function ConnectionSideRail({ side, scale, theme, onPointerDown }: { side: "left" | "right"; scale: number; theme: CanvasTheme; onPointerDown: (event: React.PointerEvent, anchorRatio: number) => void }) {
const handleRef = useRef<HTMLSpanElement>(null);
const anchorRatioRef = useRef(0.5);
const inverseScale = 1 / Math.max(scale, 0.05);
@@ -634,10 +682,6 @@ function ConnectionSideRail({ side, scale, visible, theme, onPointerDown }: { si
if (handleRef.current) handleRef.current.style.top = "50%";
}, []);
useEffect(() => {
if (!visible) resetAnchor();
}, [resetAnchor, visible]);
const updateAnchor = (event: React.PointerEvent<HTMLButtonElement>) => {
const railBounds = event.currentTarget.getBoundingClientRect();
const nodeBounds = event.currentTarget.parentElement?.getBoundingClientRect() || railBounds;
@@ -651,7 +695,7 @@ function ConnectionSideRail({ side, scale, visible, theme, onPointerDown }: { si
return (
<button
type="button"
className={`group absolute top-1/2 z-[var(--node-z-overlay)] touch-none -translate-y-1/2 outline-none transition-opacity duration-150 ${visible ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`}
className="group pointer-events-auto absolute top-1/2 z-[var(--node-z-overlay)] touch-none -translate-y-1/2 opacity-100 outline-none transition-opacity duration-150"
style={{ width: 56 * inverseScale, height: `min(100%, ${72 * inverseScale}px)`, ...(side === "left" ? { right: "100%" } : { left: "100%" }) }}
onPointerEnter={updateAnchor}
onPointerMove={updateAnchor}

View File

@@ -65,11 +65,12 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
onPointerDown={() => preloadWorkspaceRoute(tool.slug)}
onPointerEnter={() => preloadWorkspaceRoute(tool.slug)}
className={cn(
"app-workspace-nav-link app-workspace-rail-tile grid size-10 place-items-center rounded-md text-[var(--fs-tiny)] transition-colors",
"app-workspace-nav-link app-workspace-rail-tile flex min-h-12 w-full flex-col items-center justify-center gap-1 rounded-md px-1 py-1 text-[var(--fs-tiny)] leading-none transition-colors",
active ? "is-active font-medium" : "",
)}
>
<Icon className="app-workspace-nav-icon shrink-0" strokeWidth={1.8} />
<span className="truncate">{tool.label.slice(0, 2)}</span>
</Link>
);
};
@@ -121,8 +122,9 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
{/* 桌面:64px 电影胶片条式轨道 */}
<div className="app-workspace-rail hidden min-h-0 flex-col overflow-y-auto lg:flex">
<div className="app-workspace-rail-header flex h-16 shrink-0 items-center justify-center">
<Link to="/home" className="grid size-10 shrink-0 place-items-center rounded-md text-foreground/55 transition-colors hover:bg-surface-hover hover:text-foreground" title="工作台首页">
<Link to="/home" className="app-workspace-rail-tile flex min-h-12 w-10 shrink-0 flex-col items-center justify-center gap-1 rounded-md px-1 py-1 text-[var(--fs-tiny)] leading-none text-foreground/55 transition-colors hover:bg-surface-hover hover:text-foreground" title="工作台首页">
<Home className="app-workspace-nav-icon" strokeWidth={1.8} />
<span>首页</span>
</Link>
</div>
<div className="app-workspace-rail-middle flex min-h-0 w-full flex-1 flex-col items-center justify-start overflow-y-auto px-3 pt-3">

View File

@@ -1,7 +1,7 @@
import { Input, InputNumber, Segmented, Select, Switch } from "antd";
import type { ReactNode } from "react";
import { defaultImageCapabilityConfig, defaultModelCapabilityConfig, type ImageCapabilityConfig, type ModelCapabilityConfig, type TextCapabilityConfig, type VideoCapabilityConfig } from "@/lib/model-capabilities";
import { defaultImageCapabilityConfig, defaultModelCapabilityConfig, normalizeModelCapabilityConfig, type ImageCapabilityConfig, type ModelCapabilityConfig, type TextCapabilityConfig, type VideoCapabilityConfig } from "@/lib/model-capabilities";
import type { ModelProtocol } from "@/lib/model-protocols";
import { VIDEO_RESOLUTION_CAPABILITY_OPTIONS } from "@/lib/video-generation-options";
@@ -33,7 +33,7 @@ export function ModelCapabilityEditor({ value, onChange, protocol, capability =
if (capability === "image") {
return <ImageCapabilityEditor value={value} onChange={onChange} protocol={protocol} model={model} disabled={disabled} />;
}
const profile = value?.video || defaultModelCapabilityConfig(protocol).video!;
const profile = normalizeModelCapabilityConfig(value || defaultModelCapabilityConfig(protocol)).video!;
const update = (patch: Partial<VideoCapabilityConfig>) => onChange?.({ version: 1, video: { ...profile, ...patch } });
const updateReferences = (patch: Partial<VideoCapabilityConfig["references"]>) => update({ references: { ...profile.references, ...patch } });
const updateDuration = (patch: Partial<VideoCapabilityConfig["duration"]>) => update({ duration: { ...profile.duration, ...patch } });
@@ -125,7 +125,7 @@ function TextCapabilityEditor({ value, onChange, protocol, disabled }: Pick<Prop
}
function ImageCapabilityEditor({ value, onChange, protocol, model, disabled }: Required<Pick<Props, "model" | "disabled">> & Pick<Props, "value" | "onChange" | "protocol">) {
const profile = value?.image || defaultImageCapabilityConfig(protocol, model);
const profile = normalizeModelCapabilityConfig(value || { version: 1, image: defaultImageCapabilityConfig(protocol, model) }).image!;
const update = (patch: Partial<ImageCapabilityConfig>) => onChange?.({ version: 1, image: { ...profile, ...patch } });
const updateReferences = (patch: Partial<ImageCapabilityConfig["references"]>) => update({ references: { ...profile.references, ...patch } });
const updateSize = (patch: Partial<ImageCapabilityConfig["size"]>) => update({ size: { ...profile.size, ...patch } });