revert(canvas): TapNow 公开画布导入 - 撤销 main 误合并
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
const TAPNOW_SHARE_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
||||
const TAPNOW_SHARE_HOST = "app.tapnow.media";
|
||||
|
||||
export function parseTapNowShareID(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return "";
|
||||
let candidate = trimmed;
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
const hostname = parsed.hostname.toLowerCase().replace(/\.$/, "");
|
||||
if (hostname !== TAPNOW_SHARE_HOST || !/^\/tapflow\/view\/?[^/]*\/?$/.test(parsed.pathname)) return "";
|
||||
const parts = parsed.pathname.split("/").filter(Boolean);
|
||||
candidate = parts.at(-1) || "";
|
||||
} catch {
|
||||
candidate = trimmed;
|
||||
}
|
||||
return TAPNOW_SHARE_ID_PATTERN.test(candidate) ? candidate : "";
|
||||
}
|
||||
|
||||
export function formatTapNowBatchTime(value: string) {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "未知";
|
||||
const pad = (part: number) => String(part).padStart(2, "0");
|
||||
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}${pad(date.getHours())}${pad(date.getMinutes())}`;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { Bot, Check, ChevronDown, Clapperboard, CloudDownload, Coins, Focus, FolderKanban, Gauge, Home, LayoutGrid, LoaderCircle, Menu, Pencil, Plus, Redo2, Search, Settings2, Share2, Sparkles, Trash2, Undo2, Upload } from "lucide-react";
|
||||
import { Bot, Check, ChevronDown, Clapperboard, Coins, Focus, FolderKanban, Gauge, Home, LayoutGrid, LoaderCircle, Menu, Pencil, Plus, Redo2, Search, Settings2, Share2, Sparkles, Trash2, Undo2, Upload } from "lucide-react";
|
||||
import { Button, Dropdown, Modal, Tooltip } from "antd";
|
||||
|
||||
import { useWalletBalance } from "@/hooks/use-wallet-balance";
|
||||
@@ -28,7 +28,6 @@ type CanvasTopBarProps = {
|
||||
onCreateProject: () => void;
|
||||
onDeleteProject: () => void;
|
||||
onImportImage: () => void;
|
||||
onImportTapNow: () => void;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onShare: () => void;
|
||||
@@ -59,7 +58,6 @@ export function CanvasTopBar({
|
||||
onCreateProject,
|
||||
onDeleteProject,
|
||||
onImportImage,
|
||||
onImportTapNow,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onShare,
|
||||
@@ -193,9 +191,6 @@ export function CanvasTopBar({
|
||||
|
||||
<div className="canvas-topbar-cluster pointer-events-auto flex items-center gap-1.5">
|
||||
<Button type="text" className="!hidden !h-10 !w-10 !min-w-10 !rounded-xl !p-0 lg:!inline-flex" style={{ color: theme.node.text }} icon={<Search className="size-4" />} onClick={onOpenSearch} aria-label="搜索画布节点" title="搜索画布节点" />
|
||||
<Button type="text" className="canvas-topbar-import-button !h-10 !rounded-xl !px-2.5 !font-medium" style={{ color: theme.node.text }} icon={<CloudDownload className="size-4" />} onClick={onImportTapNow} aria-label="导入 TapNow 画布" title="导入 TapNow 画布">
|
||||
<span className="hidden xl:inline">导入 TapNow</span>
|
||||
</Button>
|
||||
<Dropdown
|
||||
trigger={["click"]}
|
||||
menu={{
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import { App, Button, Input, Modal, Tag } from "antd";
|
||||
import { CircleAlert, ExternalLink, Import, LoaderCircle } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { formatTapNowBatchTime, parseTapNowShareID } from "@/lib/canvas/tapnow-import";
|
||||
import { importTapNowCanvas, type TapNowImportResult } from "@/services/api/tapnow";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type ViewportTransform } from "@/types/canvas";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
projectId: string;
|
||||
viewport: ViewportTransform;
|
||||
viewportSize: { width: number; height: number };
|
||||
onClose: () => void;
|
||||
onApply: (nodes: CanvasNodeData[], connections: CanvasConnection[]) => Promise<void>;
|
||||
};
|
||||
|
||||
function buildCanvasNodes(result: TapNowImportResult, viewport: ViewportTransform, viewportSize: { width: number; height: number }) {
|
||||
const minX = Math.min(...result.nodes.map((node) => node.x));
|
||||
const minY = Math.min(...result.nodes.map((node) => node.y));
|
||||
const maxX = Math.max(...result.nodes.map((node) => node.x + node.width));
|
||||
const maxY = Math.max(...result.nodes.map((node) => node.y + node.height));
|
||||
const offsetX = (viewportSize.width / 2 - viewport.x) / viewport.k - (minX + maxX) / 2;
|
||||
const offsetY = (viewportSize.height / 2 - viewport.y) / viewport.k - (minY + maxY) / 2;
|
||||
return result.nodes.map<CanvasNodeData>((node) => ({
|
||||
id: node.id,
|
||||
type: node.type === "video" ? CanvasNodeType.Video : node.type === "audio" ? CanvasNodeType.Audio : node.type === "text" ? CanvasNodeType.Text : CanvasNodeType.Image,
|
||||
title: node.title,
|
||||
position: { x: node.x + offsetX, y: node.y + offsetY },
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
metadata: {
|
||||
content: node.content,
|
||||
prompt: node.prompt,
|
||||
model: node.model,
|
||||
size: node.size,
|
||||
quality: node.quality,
|
||||
seconds: node.seconds,
|
||||
vquality: node.vquality,
|
||||
generateAudio: node.generateAudio,
|
||||
status: node.status || "idle",
|
||||
errorDetails: node.errorDetails,
|
||||
naturalWidth: node.naturalWidth,
|
||||
naturalHeight: node.naturalHeight,
|
||||
durationMs: node.durationMs,
|
||||
mimeType: node.mimeType,
|
||||
importSource: node.metadata,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
export function TapNowImportDialog({ open, projectId, viewport, viewportSize, onClose, onApply }: Props) {
|
||||
const { message } = App.useApp();
|
||||
const [value, setValue] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<TapNowImportResult | null>(null);
|
||||
const shareID = useMemo(() => parseTapNowShareID(value), [value]);
|
||||
|
||||
const reset = () => {
|
||||
setValue("");
|
||||
setResult(null);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
if (loading) return;
|
||||
reset();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const changeValue = (nextValue: string) => {
|
||||
setValue(nextValue);
|
||||
setResult(null);
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
if (!shareID) {
|
||||
message.error("请填写有效的 TapNow 画布分享链接或分享 ID");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
setResult(await importTapNowCanvas(projectId, shareID));
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "读取 TapNow 画布失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const apply = async () => {
|
||||
if (!result) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await onApply(buildCanvasNodes(result, viewport, viewportSize), result.connections);
|
||||
reset();
|
||||
onClose();
|
||||
message.success(`已导入 ${result.importedNodeCount} 个节点和 ${result.importedConnectionCount} 条连接`);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "保存导入结果失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hasWarnings = result ? Boolean(result.skippedNodes.length || result.skippedConnections.length || result.multiResultNodeCount || result.reusedFailedNodeCount || result.placeholderNodeCount || result.warnings.length) : false;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="workspace-modal"
|
||||
open={open}
|
||||
onCancel={close}
|
||||
title="导入 TapNow 画布"
|
||||
width={620}
|
||||
footer={
|
||||
result ? (
|
||||
[
|
||||
<Button key="close" onClick={close}>
|
||||
关闭
|
||||
</Button>,
|
||||
<Button key="apply" type="primary" icon={<Import className="size-4" />} loading={loading} onClick={() => void apply()}>
|
||||
确认导入
|
||||
</Button>,
|
||||
]
|
||||
) : (
|
||||
<Button type="primary" loading={loading} onClick={() => void load()}>
|
||||
读取画布
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium">TapNow 画布分享链接或分享 ID</label>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(event) => changeValue(event.target.value)}
|
||||
placeholder="粘贴 https://app.tapnow.media/tapflow/view/…"
|
||||
disabled={loading}
|
||||
suffix={shareID && value !== shareID ? <ExternalLink className="size-4 text-foreground/35" /> : null}
|
||||
/>
|
||||
</div>
|
||||
{loading && !result ? (
|
||||
<div className="flex items-center gap-2 text-sm text-foreground/55">
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
正在读取 TapNow 画布…
|
||||
</div>
|
||||
) : null}
|
||||
{result ? (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-xl p-4" style={{ background: "var(--library-surface)" }}>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold">{result.projectName || "TapNow 画布"}</div>
|
||||
<div className="mt-1 text-sm text-foreground/60">
|
||||
可导入 {result.importedNodeCount} 个节点 · {result.importedConnectionCount} 条连线
|
||||
</div>
|
||||
</div>
|
||||
<Tag color="blue">批次:{formatTapNowBatchTime(result.batchCreatedAt)}</Tag>
|
||||
</div>
|
||||
<div className="mt-3 text-xs leading-5 text-foreground/50">支持图片、视频、音频和文本节点;节点会保留相对位置,并整体放到当前可视区域中心。</div>
|
||||
</div>
|
||||
{hasWarnings ? (
|
||||
<div className="flex items-start gap-2 rounded-lg px-3 py-2.5 text-xs leading-5 text-foreground/60" style={{ background: "var(--surface-hover)" }}>
|
||||
<CircleAlert className="mt-0.5 size-4 shrink-0" />
|
||||
<div>
|
||||
{result.skippedNodes.length ? <div>{result.skippedNodes.length} 个暂不支持的节点未导入,相关连线已自动忽略。</div> : null}
|
||||
{!result.skippedNodes.length && result.skippedConnections.length ? <div>{result.skippedConnections.length} 条无效连线已自动忽略。</div> : null}
|
||||
{result.reusedFailedNodeCount ? <div>{result.reusedFailedNodeCount} 个最近任务失败但仍有历史结果的节点已保留。</div> : null}
|
||||
{result.placeholderNodeCount ? <div>{result.placeholderNodeCount} 个尚未生成结果的节点已作为占位节点保留。</div> : null}
|
||||
{result.multiResultNodeCount ? <div>{result.multiResultNodeCount} 个多结果节点已使用首个结果。</div> : null}
|
||||
{result.warnings.map((warning, index) => (
|
||||
<div key={`${warning.id || "warning"}-${index}`}>{warning.message}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Tag>等待确认导入</Tag>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -72,7 +72,6 @@ import { deriveStoryboardPipelineProgress } from "@/lib/canvas/canvas-storyboard
|
||||
import { CanvasAgentChangeToast, CanvasMergeStatusToast, CanvasUploadStatusToast } from "./canvas-project-feedback";
|
||||
import { backendProviderConfig, getGenerationCount } from "@/lib/canvas/canvas-project-generation";
|
||||
import { CanvasTopBar } from "./canvas-project-top-bar";
|
||||
import { TapNowImportDialog } from "./components/tapnow-import-dialog";
|
||||
import { CanvasFocusModeBar } from "@/components/canvas/canvas-focus-mode-bar";
|
||||
import { CanvasProjectContextMenu } from "./canvas-project-context-menu";
|
||||
import { CanvasProjectMediaDialogs } from "./canvas-project-media-dialogs";
|
||||
@@ -191,7 +190,6 @@ function InfiniteCanvasPage() {
|
||||
const [workspaceMode, setWorkspaceMode] = useState<CanvasWorkspaceMode>(readCanvasWorkspaceMode);
|
||||
const [clearConfirmOpen, setClearConfirmOpen] = useState(false);
|
||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
||||
const [tapNowImportOpen, setTapNowImportOpen] = useState(false);
|
||||
const [nodeSearchOpen, setNodeSearchOpen] = useState(false);
|
||||
const [toolbarNodeId, setToolbarNodeId] = useState<string | null>(null);
|
||||
const [nodeImageSettingsOpen, setNodeImageSettingsOpen] = useState(false);
|
||||
@@ -333,28 +331,6 @@ function InfiniteCanvasPage() {
|
||||
cleanupAssetImages,
|
||||
cleanupCanvasFiles,
|
||||
});
|
||||
|
||||
const applyTapNowImport = useCallback(
|
||||
async (importedNodes: CanvasNodeData[], importedConnections: CanvasConnection[]) => {
|
||||
const previousNodes = nodesRef.current;
|
||||
const previousConnections = connectionsRef.current;
|
||||
const nextNodes = [...nodesRef.current, ...importedNodes];
|
||||
const nextConnections = [...connectionsRef.current, ...importedConnections];
|
||||
nodesRef.current = nextNodes;
|
||||
connectionsRef.current = nextConnections;
|
||||
setNodes(nextNodes);
|
||||
setConnections(nextConnections);
|
||||
const saved = await saveCanvasProject();
|
||||
if (!saved) {
|
||||
nodesRef.current = previousNodes;
|
||||
connectionsRef.current = previousConnections;
|
||||
setNodes(previousNodes);
|
||||
setConnections(previousConnections);
|
||||
throw new Error("画布保存失败,已撤销本次 TapNow 导入");
|
||||
}
|
||||
},
|
||||
[saveCanvasProject, setConnections, setNodes],
|
||||
);
|
||||
const linkedProjectId = shortDramaEnabled ? currentProject?.projectId || "" : "";
|
||||
const linkedProjectQuery = useQuery({ queryKey: ["project", linkedProjectId], queryFn: () => getProject(linkedProjectId), enabled: Boolean(linkedProjectId) });
|
||||
const refetchLinkedProject = linkedProjectQuery.refetch;
|
||||
@@ -1663,7 +1639,6 @@ function InfiniteCanvasPage() {
|
||||
onCreateProject={createAndOpenProject}
|
||||
onDeleteProject={deleteCurrentProject}
|
||||
onImportImage={() => handleUploadRequest()}
|
||||
onImportTapNow={() => setTapNowImportOpen(true)}
|
||||
onUndo={undoCanvas}
|
||||
onRedo={redoCanvas}
|
||||
onShare={() => setShareModalOpen(true)}
|
||||
@@ -1711,7 +1686,6 @@ function InfiniteCanvasPage() {
|
||||
) : null}
|
||||
|
||||
<CanvasShareModal projectId={projectId} open={shareModalOpen} onClose={() => setShareModalOpen(false)} beforeCreate={saveCanvasProject} />
|
||||
<TapNowImportDialog open={tapNowImportOpen} projectId={projectId} viewport={viewport} viewportSize={size} onClose={() => setTapNowImportOpen(false)} onApply={applyTapNowImport} />
|
||||
|
||||
<CanvasStylePickerModal open={stylePickerOpen} value={activeStylePresetId} applying={styleApplying} onClose={() => setStylePickerOpen(false)} onSelect={selectCanvasStyle} />
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import type { CanvasConnection } from "@/types/canvas";
|
||||
import { apiClient, request } from "@/services/api/request";
|
||||
|
||||
export type TapNowImportIssue = { id?: string; name?: string; reason: string };
|
||||
export type TapNowImportWarning = { id?: string; message: string };
|
||||
|
||||
export type TapNowImportNode = {
|
||||
id: string;
|
||||
type: "image" | "video" | "audio" | "text";
|
||||
title: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
content: string;
|
||||
prompt?: string;
|
||||
model?: string;
|
||||
size?: string;
|
||||
quality?: string;
|
||||
seconds?: string;
|
||||
vquality?: string;
|
||||
generateAudio?: string;
|
||||
naturalWidth?: number;
|
||||
naturalHeight?: number;
|
||||
durationMs?: number;
|
||||
mimeType?: string;
|
||||
status?: "idle" | "success" | "error";
|
||||
errorDetails?: string;
|
||||
metadata: {
|
||||
provider: "tapnow";
|
||||
shareId: string;
|
||||
nodeId: string;
|
||||
batchId: string;
|
||||
sourceType?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TapNowImportResult = {
|
||||
batchId: string;
|
||||
batchCreatedAt: string;
|
||||
shareId: string;
|
||||
projectName: string;
|
||||
nodes: TapNowImportNode[];
|
||||
connections: CanvasConnection[];
|
||||
importedNodeCount: number;
|
||||
importedConnectionCount: number;
|
||||
skippedNodes: TapNowImportIssue[];
|
||||
skippedConnections: TapNowImportIssue[];
|
||||
warnings: TapNowImportWarning[];
|
||||
multiResultNodeCount: number;
|
||||
reusedFailedNodeCount: number;
|
||||
placeholderNodeCount: number;
|
||||
};
|
||||
|
||||
export function importTapNowCanvas(projectId: string, shareId: string) {
|
||||
return request<TapNowImportResult>(apiClient.post(`/canvas-projects/${encodeURIComponent(projectId)}/import/tapnow`, { shareId }));
|
||||
}
|
||||
@@ -142,13 +142,6 @@ export type CanvasSkillSnapshot = {
|
||||
};
|
||||
|
||||
export type CanvasNodeMetadata = {
|
||||
importSource?: {
|
||||
provider: "tapnow";
|
||||
shareId: string;
|
||||
nodeId: string;
|
||||
batchId: string;
|
||||
sourceType?: string;
|
||||
};
|
||||
content?: string;
|
||||
richText?: Record<string, unknown>;
|
||||
composerContent?: string;
|
||||
|
||||
Reference in New Issue
Block a user