"use client" import { type ReactNode, useEffect, useRef } from "react" import { Handle, Position } from "@xyflow/react" import { CheckCircle2, Loader2, AlertCircle, Pin } from "lucide-react" import { ResizeRight, ResizeBR } from "./resize-handle" // 节点正文 zoom 范围:基准 320px (= 1),到 640px 时放大到 ~1.6 const BASE_WIDTH = 320 const MIN_ZOOM = 1 const MAX_ZOOM = 1.6 export type NodeKind = "input" | "process" | "ai" | "output" export type NodeStatus = "pending" | "running" | "done" | "failed" interface Props { type: NodeKind status: NodeStatus icon?: ReactNode title: string subtitle?: string width?: number | string // 默认 '100%':让外层 ReactFlow node wrapper (node.style.width) 决定,配合 NodeResizeControl 用户可拖 selected?: boolean hasTarget?: boolean hasSource?: boolean pinned?: boolean // 钉下 → 锁定位置与尺寸,不可拖动、不显示 resize 把手 onTogglePin?: () => void children?: ReactNode } const STATUS_DOT: Record = { pending: "status-dot", running: "status-dot status-dot--running", done: "status-dot status-dot--done", failed: "status-dot status-dot--failed", } const STATUS_LABEL: Record = { pending: "待运行", running: "运行中", done: "完成", failed: "失败", } export function NodeShell({ type, status, icon, title, subtitle, width = "100%", selected, hasTarget = true, hasSource = true, pinned = false, onTogglePin, children, }: Props) { const rootRef = useRef(null) const bodyRef = useRef(null) // 卡片宽度变化 → 给 body 设 zoom,让内部文字 / 按钮 / 间距按比例放大 useEffect(() => { const root = rootRef.current const body = bodyRef.current if (!root || !body) return const ro = new ResizeObserver(() => { const w = root.clientWidth const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, w / BASE_WIDTH)) body.style.zoom = String(zoom) }) ro.observe(root) return () => ro.disconnect() }, []) return (
{hasTarget && }
{icon ? {icon} : null} {title} {status === "running" ? : status === "done" ? : status === "failed" ? : null} {onTogglePin && ( )}
{subtitle && (
{subtitle} · {STATUS_LABEL[status]}
)} {children}
{hasSource && } {!pinned && } {!pinned && }
) }