import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { open } from "@tauri-apps/plugin-dialog"; import { batchCreateProjects, getSpaces, scanSubfolders, type SubFolder } from "../../lib/commands"; import { useUIStore } from "../../store/ui"; import { Dialog } from "../ui/Dialog"; // Windows 路径转 WSL 路径(仅 /mnt/x/... 形式,不处理 UNC) function winToWsl(p: string): string { const norm = p.replace(/\\/g, "/"); const m = norm.match(/^([a-zA-Z]):\/(.*)$/); return m ? `/mnt/${m[1].toLowerCase()}/${m[2]}` : norm; } type PathMode = "win" | "wsl_convert" | "wsl_manual"; interface FolderItem extends SubFolder { checked: boolean; customId: string; // 可编辑的 id customName: string; // 可编辑的显示名 } export function BatchAddModal({ onClose }: { onClose: () => void }) { const showToast = useUIStore((s) => s.showToast); const qc = useQueryClient(); const { data: spaces = [] } = useQuery({ queryKey: ["spaces"], queryFn: getSpaces }); const currentSpace = spaces.find((s) => s.is_current); const [parentPath, setParentPath] = useState(""); const [pathMode, setPathMode] = useState("win"); const [items, setItems] = useState([]); const [scanning, setScanning] = useState(false); const [saving, setSaving] = useState(false); // 公共字段 const [commonStatus, setCommonStatus] = useState("active"); const [commonPriority, setCommonPriority] = useState("mid"); const pickFolder = async () => { const selected = await open({ directory: true, multiple: false }); if (!selected) return; setParentPath(selected as string); setItems([]); }; const scan = async () => { if (!parentPath.trim()) { showToast("❌ 请先选择父目录"); return; } setScanning(true); try { const folders = await scanSubfolders(parentPath.trim()); setItems(folders.map((f) => ({ ...f, checked: true, customId: f.name.toLowerCase().replace(/[^a-z0-9_-]/g, "-"), customName: f.name, }))); } catch (e) { showToast(`❌ ${e}`); } finally { setScanning(false); } }; const toggleAll = (val: boolean) => setItems((prev) => prev.map((i) => ({ ...i, checked: val }))); const update = (idx: number, patch: Partial) => setItems((prev) => prev.map((item, i) => i === idx ? { ...item, ...patch } : item)); const handleSave = async () => { const selected = items.filter((i) => i.checked); if (selected.length === 0) { showToast("❌ 请至少勾选一个文件夹"); return; } // 检查 id 是否有重复 const ids = selected.map((i) => i.customId.trim()); const dupId = ids.find((id, idx) => ids.indexOf(id) !== idx); if (dupId) { showToast(`❌ ID 重复:${dupId}`); return; } if (ids.some((id) => !id)) { showToast("❌ 存在空 ID,请检查"); return; } setSaving(true); try { const payload = selected.map((item) => { const winPath = item.path; const wslPath = pathMode === "wsl_convert" ? winToWsl(item.path) : undefined; return { id: item.customId.trim(), name: item.customName.trim() || item.name, win_path: pathMode === "win" ? winPath : undefined, wsl_path: pathMode === "wsl_convert" ? wslPath : undefined, status: commonStatus, priority: commonPriority, space_id: currentSpace?.id, platform: pathMode === "wsl_convert" ? "wsl" : "windows", }; }); const skipped = await batchCreateProjects(payload); qc.invalidateQueries({ queryKey: ["projects"] }); if (skipped.length > 0) { showToast(`已添加 ${selected.length - skipped.length} 个,跳过 ${skipped.length} 个(ID 已存在或目录已登记)`); } else { showToast(`成功添加 ${selected.length} 个项目 ✓`); } onClose(); } catch (e) { showToast(`❌ ${e}`); } finally { setSaving(false); } }; const checkedCount = items.filter((i) => i.checked).length; return (
{/* Step 1: 选目录 */}

① 选择父目录

{ setParentPath(e.target.value); setItems([]); }} placeholder="C:\projects 或手动输入" />
{/* Step 2: 路径模式 */} {items.length > 0 && (

② 路径存储方式

{([ ["win", "仅 Windows 路径"], ["wsl_convert", "仅 WSL 路径(自动转换)"], ] as [PathMode, string][]).map(([val, label]) => ( ))}
{pathMode === "wsl_convert" && (

示例:C:\projects\app → /mnt/c/projects/app

)}
)} {/* Step 3: 公共字段 */} {items.length > 0 && (

③ 公共属性

)} {/* Step 4: 文件夹列表 */} {items.length > 0 && (

④ 选择文件夹 已选 {checkedCount} / {items.length}

{items.map((item, idx) => ( ))}
项目 ID 显示名称 文件夹名
update(idx, { checked: e.target.checked })} className="accent-blue-600 cursor-pointer" /> update(idx, { customId: e.target.value })} /> update(idx, { customName: e.target.value })} /> {item.name}
)} {items.length === 0 && !scanning && parentPath && (
点击「扫描」加载文件夹列表
)}
); }