import { useState, useEffect } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { scanProjectEnv, getProjectEnvTools, saveProjectEnvTools, getToolchainStatus, prepareToolchain, type Project, type EnvToolItem, type SaveEnvToolInput, type RuntimeToolStatus, type ToolchainStatus, } from "../../lib/commands"; import { confirm } from "@tauri-apps/plugin-dialog"; import { useUIStore } from "../../store/ui"; // ── 层元信息 ─────────────────────────────────────────────────────────────────── const LAYER_META: Record = { 0: { label: "第 0 层", desc: "版本管理器", color: "bg-purple-50 border-purple-200 text-purple-800" }, 1: { label: "第 1 层", desc: "语言运行时", color: "bg-blue-50 border-blue-200 text-blue-800" }, 2: { label: "第 2 层", desc: "直接使用工具", color: "bg-green-50 border-green-200 text-green-800" }, 3: { label: "第 3 层", desc: "局部内置", color: "bg-gray-50 border-gray-200 text-gray-700" }, }; const TOOLCHAIN_STATUS_META: Record = { ready_managed: { label: "已由 mise 管理", color: "text-green-700 bg-green-50 border-green-200" }, compliant_unmanaged: { label: "版本匹配但未托管", color: "text-amber-700 bg-amber-50 border-amber-200" }, mismatch: { label: "版本不匹配", color: "text-red-700 bg-red-50 border-red-200" }, conflict: { label: "声明冲突", color: "text-red-700 bg-red-50 border-red-200" }, missing_declaration: { label: "缺少声明", color: "text-amber-700 bg-amber-50 border-amber-200" }, unavailable: { label: "当前不可用", color: "text-gray-600 bg-gray-50 border-gray-200" }, }; // ── 主组件 ──────────────────────────────────────────────────────────────────── interface Props { project: Project; onClose: () => void; } export function ProjectEnvModal({ project, onClose }: Props) { const showToast = useUIStore((s) => s.showToast); const qc = useQueryClient(); const [items, setItems] = useState([]); const [scanning, setScanning] = useState(false); const [saving, setSaving] = useState(false); const [preparing, setPreparing] = useState(false); const [scanned, setScanned] = useState(false); // 加载已保存的数据 const { data: savedItems = [], isLoading } = useQuery({ queryKey: ["projectEnvTools", project.id], queryFn: () => getProjectEnvTools(project.id), }); const { data: toolchain, isLoading: toolchainLoading, error: toolchainError, refetch: refetchToolchain, } = useQuery({ queryKey: ["toolchainStatus", project.id], queryFn: () => getToolchainStatus(project.id), }); // 已保存数据加载后,若尚未扫描则显示已保存内容 useEffect(() => { if (savedItems.length > 0 && !scanned) { setItems(savedItems); } }, [savedItems, scanned]); const handleScan = async (env: "win" | "wsl") => { setScanning(true); try { const result = await scanProjectEnv(project.id, env); setItems(result); setScanned(true); showToast(`扫描完成,发现 ${result.length} 项工具 ✓`); } catch (e) { showToast(`❌ ${e}`); } finally { setScanning(false); } }; const handleRemove = (id: string) => { setItems((prev) => prev.filter((item) => item.id !== id)); }; const handleSave = async () => { setSaving(true); try { const inputs: SaveEnvToolInput[] = items.map((item) => ({ id: item.id, layer: item.layer, name: item.name, source: item.source, version: item.version, note: item.note, })); await saveProjectEnvTools(project.id, inputs); qc.invalidateQueries({ queryKey: ["projectEnvTools", project.id] }); showToast("已保存 ✓"); onClose(); } catch (e) { showToast(`❌ ${e}`); } finally { setSaving(false); } }; const handlePrepareToolchain = async () => { const confirmed = await confirm( "炼境将通过 mise install 下载并准备项目已声明的 Node/pnpm 版本。不会修改 package.json 或 lockfile,是否继续?", { kind: "warning" }, ); if (!confirmed) return; setPreparing(true); try { const result = await prepareToolchain(project.id, true); await refetchToolchain(); showToast(result.verified ? `工具链准备并验证完成:${result.command.join(" ")} ✓` : `⚠️ ${result.command.join(" ")} 已执行,但二次诊断尚未达到 ready_managed,请查看状态证据`); } catch (e) { showToast(`❌ ${e}`); } finally { setPreparing(false); } }; // 按层分组 const grouped = [0, 1, 2, 3].reduce>((acc, layer) => { const layerItems = items.filter((item) => item.layer === layer); if (layerItems.length > 0) acc[layer] = layerItems; return acc; }, {}); return (
{/* Header */}

环境工具

{project.name}

{/* Scan buttons */}
扫描目录: {project.win_path && ( )} {project.wsl_path && ( )} 扫描后结果自动替换,确认后点保存
{/* Content */}
{isLoading && !scanned ? (

加载中…

) : items.length === 0 ? (

🔍

点击上方「扫描」按钮分析项目目录

会根据配置文件自动推断技术栈和工具链

) : (
{/* 三层模型说明 */}
{[0, 1, 2, 3].map((l) => ( {LAYER_META[l].label} {LAYER_META[l].desc} ))}
{/* 分层展示 */} {Object.entries(grouped).map(([layerStr, layerItems]) => { const layer = Number(layerStr); const meta = LAYER_META[layer]; return (
{meta.label} {meta.desc}
{layerItems.map((item) => ( handleRemove(item.id)} /> ))}
); })}
)}
{/* Footer */}
{items.length} 项工具
); } function ToolchainCard({ status, loading, error, preparing, onPrepare, }: { status?: ToolchainStatus; loading: boolean; error: unknown; preparing: boolean; onPrepare: () => void; }) { if (loading) { return
正在检查项目工具链…
; } if (error || !status) { return (
工具链状态不可用:{String(error ?? "未返回状态")}
); } const needsPrepare = [status.node.status, status.pnpm.status].some((value) => ["mismatch", "unavailable", "compliant_unmanaged"].includes(value), ); const canPrepare = needsPrepare && status.conflicts.length === 0 && status.resolver.configured && Boolean(status.resolver.executable); return (

项目工具链契约

{status.project_root}

{canPrepare && ( )}
mise resolver
{status.resolver.status} {status.resolver.actual_version && {status.resolver.actual_version}} {status.resolver.executable &&

{status.resolver.executable}

}
{status.conflicts.length > 0 && (

冲突声明:{status.conflicts.join("、")}。炼境不会替项目选择版本。

)} {needsPrepare && !canPrepare && status.conflicts.length === 0 && (

{!status.resolver.configured ? "项目尚未配置 mise.toml,请先应用工具链能力包。" : "本机尚未安装 mise,请先完成一次用户级安装。"}

)}
); } function ToolchainRow({ label, item }: { label: string; item: RuntimeToolStatus }) { const meta = TOOLCHAIN_STATUS_META[item.status] ?? TOOLCHAIN_STATUS_META.unavailable; return (
{label} {meta.label}
期望:{item.declared_version ?? "未声明"} 实际:{item.actual_version ?? "不可用"}

{item.declared_source ?? "无声明来源"}{item.executable ? ` · ${item.executable}` : ""}

{item.recommendation &&

{item.recommendation}

}
); } // ── 工具行 ──────────────────────────────────────────────────────────────────── function EnvToolRow({ item, onRemove }: { item: EnvToolItem; onRemove: () => void }) { return (
{item.name} {item.source && ( via {item.source} )} {item.version && ( {item.version} )}
{item.note && (

{item.note}

)}
); }