import { useState, useEffect, useRef } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { listen } from "@tauri-apps/api/event"; import { listTemplates, createProjectFromTemplate, TemplateVariant, } from "../../lib/commands"; import { useUIStore } from "../../store/ui"; interface Props { spaceId?: string; onClose: () => void; } type Step = "config" | "template" | "executing" | "done"; const PLATFORM_LABEL: Record = { both: "通用", win: "仅 Win", wsl: "仅 WSL", }; const EXEC_STEPS = [ { id: 1, label: "创建文件结构", icon: "📁" }, { id: 2, label: "安装依赖", icon: "📦" }, { id: 3, label: "注册项目", icon: "🗃️" }, ]; export function NewProjectModal({ spaceId, onClose }: Props) { const showToast = useUIStore((s) => s.showToast); const qc = useQueryClient(); // ── Step 1 ──────────────────────────────────────────────────────────────── const [platform, setPlatform] = useState<"win" | "wsl">("win"); const [parentPath, setParentPath] = useState(""); const [projectName, setProjectName] = useState(""); // ── Step 2 ──────────────────────────────────────────────────────────────── const [selectedTemplateId, setSelectedTemplateId] = useState(null); const [selectedVariantIdx, setSelectedVariantIdx] = useState(0); // ── Step 3/4 ────────────────────────────────────────────────────────────── const [logs, setLogs] = useState([]); const [step, setStep] = useState("config"); const [execStep, setExecStep] = useState(0); // 1 / 2 / 3 const [currentCmd, setCurrentCmd] = useState(""); // 当前执行的命令 const [elapsed, setElapsed] = useState(0); // 秒 const [hasError, setHasError] = useState(false); const logEndRef = useRef(null); const timerRef = useRef | null>(null); // 日志自动滚到底 useEffect(() => { logEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [logs]); // 计时器:执行时启动,完成时停止 useEffect(() => { if (step === "executing") { setElapsed(0); timerRef.current = setInterval(() => setElapsed((s) => s + 1), 1000); } else { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } } return () => { if (timerRef.current) clearInterval(timerRef.current); }; }, [step]); const { data: templates = [] } = useQuery({ queryKey: ["templates"], queryFn: listTemplates, }); const filteredTemplates = templates.filter( (t) => t.platform === "both" || t.platform === platform ); const selectedTemplate = filteredTemplates.find((t) => t.id === selectedTemplateId) ?? null; const selectedVariants: TemplateVariant[] = (() => { try { return selectedTemplate?.variants ? JSON.parse(selectedTemplate.variants) : []; } catch { return []; } })(); const projectFullPath = parentPath && projectName ? `${parentPath.replace(/[/\\]+$/, "")}\\${projectName}` : ""; // ── 选择文件夹 ───────────────────────────────────────────────────────────── const pickFolder = async () => { try { const { open } = await import("@tauri-apps/plugin-dialog"); const selected = await open({ directory: true, multiple: false }); if (typeof selected === "string") setParentPath(selected); } catch (e) { showToast(`❌ 选择文件夹失败: ${e}`); } }; // ── 执行创建 ────────────────────────────────────────────────────────────── const handleCreate = async () => { if (!selectedTemplateId || !parentPath || !projectName.trim()) return; setStep("executing"); setLogs([]); setExecStep(0); setCurrentCmd(""); setHasError(false); // 监听后端流式事件 const unlisten = await listen("template:progress", (event) => { const msg = event.payload; if (msg.startsWith("__step:")) { setExecStep(parseInt(msg.slice(7))); } else if (msg.startsWith("__cmd:")) { setCurrentCmd(msg.slice(6)); } else { setLogs((prev) => [...prev, msg]); if (msg.startsWith("❌") || msg.includes("✗")) setHasError(true); } }); try { const variantCmd = selectedVariants.length > 0 ? selectedVariants[selectedVariantIdx]?.initCommands : undefined; await createProjectFromTemplate( selectedTemplateId, parentPath, projectName.trim(), platform, spaceId, variantCmd, ); qc.invalidateQueries({ queryKey: ["projects"] }); setStep("done"); } catch (e) { setLogs((prev) => [...prev, `❌ 创建失败: ${e}`]); setHasError(true); setStep("done"); } finally { unlisten(); } }; const fmtTime = (s: number) => s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`; // ── 渲染 ────────────────────────────────────────────────────────────────── return (
{/* 标题栏 */}

从模板新建项目

{(["config", "template", "executing", "done"] as Step[]).map((s, i) => (
{i > 0 && } {s === "config" ? "基本配置" : s === "template" ? "选择模板" : s === "executing" ? "执行中" : "完成"}
))}
{/* 内容区 */}
{/* ── Step 1:基本配置 ── */} {step === "config" && ( <> {/* 平台 */}
{(["win", "wsl"] as const).map((p) => ( ))}
{/* 父目录 */}
setParentPath(e.target.value)} placeholder={platform === "wsl" ? "\\\\wsl.localhost\\Ubuntu-22.04\\home\\user\\projects" : "C:\\Users\\user\\projects"} />
{platform === "wsl" && (

WSL 路径请在文件管理器中导航到 \\wsl.localhost\发行版名\...

)}
{/* 项目名称 */}
setProjectName(e.target.value)} placeholder="my-awesome-project" />
{/* 路径预览 */} {projectFullPath && (
将创建: {projectFullPath}
)} )} {/* ── Step 2:选择模板 ── */} {step === "template" && (

选择一个模板来初始化项目结构

{filteredTemplates.length === 0 && (

没有适用于当前平台的模板

)} {filteredTemplates.map((t) => { const isSelected = selectedTemplateId === t.id; const variants: TemplateVariant[] = (() => { try { return t.variants ? JSON.parse(t.variants) : []; } catch { return []; } })(); return (
{/* variant 下拉:仅在选中且有 variants 时展开 */} {isSelected && variants.length > 0 && (

选择初始化模板

{variants.map((v, idx) => ( ))}
)}
); })}
)} {/* ── Step 3/4:执行进度 ── */} {(step === "executing" || step === "done") && (
{/* 步骤指示器 */}
{EXEC_STEPS.map((s) => { const isDone = step === "done" ? execStep >= s.id : execStep > s.id; const isActive = step === "executing" && execStep === s.id; return (
{/* 图标 */}
{isDone ? "✓" : isActive ? ( ) : s.id}
{/* 标签 */}
{s.icon} {s.label} {isActive && s.id === 2 && currentCmd && ( {currentCmd} )}
{/* 进度条(仅当前步骤显示)*/} {isActive && (
)}
{/* 计时 */} {isActive && ( {fmtTime(elapsed)} )} {isDone && s.id === execStep - 1 && step === "executing" && ( )}
); })}
{/* 实时日志(可滚动,最近 200 行)*/}
输出日志 {step === "executing" && ( ● 实时 )}
{logs.length === 0 && (
等待输出…
)} {logs.slice(-200).map((line, i) => (
{line}
))}
{/* 完成提示 */} {step === "done" && !hasError && (
🎉 项目已创建完成!共耗时 {fmtTime(elapsed)}
)} {step === "done" && hasError && (
⚠️ 执行过程中出现错误,请查看日志
)}
)}
{/* 底部按钮 */}
{step === "template" && ( )}
{step === "config" && ( )} {step === "template" && ( )}
); }