- 新增发布到GitHub功能(scan/init/commit/push/register全流程) - 新增WSL发行版检测与UNC路径自动转换,路径存储改为Windows可访问格式 - 修复libgit2访问WSL UNC路径的owner验证问题(启动时全局禁用) - ProjectCard新增本地项目/本地仓库/远程仓库三灯状态指示 - ProjectModal重构为基本信息+部署信息双标签页,固定高度稳定切换 - 添加项目支持顶部快速选择文件夹并自动填充ID和名称 - 设置页WSL区块改为下拉检测选择发行版 - Dialog组件支持fixedHeight固定高度模式
245 lines
9.3 KiB
TypeScript
245 lines
9.3 KiB
TypeScript
import { useState } from "react";
|
||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||
import {
|
||
githubListRepos,
|
||
githubGetToken,
|
||
githubGetAccount,
|
||
gitClone,
|
||
createProject,
|
||
getProjects,
|
||
getSpaces,
|
||
type GithubRepo,
|
||
} from "../../lib/commands";
|
||
import { syncProjectCatalog } from "../../lib/spacesSync";
|
||
import { useUIStore } from "../../store/ui";
|
||
|
||
interface Props {
|
||
onClose: () => void;
|
||
}
|
||
|
||
export function ImportRepoModal({ onClose }: Props) {
|
||
const queryClient = useQueryClient();
|
||
const showToast = useUIStore((s) => s.showToast);
|
||
const [search, setSearch] = useState("");
|
||
const [selected, setSelected] = useState<GithubRepo | null>(null);
|
||
const [localPath, setLocalPath] = useState("");
|
||
const [upstreamUrl, setUpstreamUrl] = useState("");
|
||
const [cloning, setCloning] = useState(false);
|
||
|
||
const { data: repos = [], isLoading, error } = useQuery({
|
||
queryKey: ["github-repos"],
|
||
queryFn: async () => {
|
||
const token = await githubGetToken();
|
||
if (!token) throw new Error("未登录 GitHub");
|
||
return githubListRepos(token);
|
||
},
|
||
});
|
||
|
||
const { data: spaces = [] } = useQuery({
|
||
queryKey: ["spaces"],
|
||
queryFn: getSpaces,
|
||
});
|
||
|
||
const currentSpace = spaces.find((s) => s.is_current);
|
||
|
||
const { data: existingProjects = [] } = useQuery({
|
||
queryKey: ["projects", currentSpace?.id],
|
||
queryFn: () => getProjects(undefined, currentSpace?.id),
|
||
enabled: !!currentSpace,
|
||
});
|
||
|
||
// 当前空间已有的 repo_url 集合(用于判重)
|
||
const importedUrls = new Set(
|
||
existingProjects.map((p) => p.repo_url).filter((u): u is string => !!u)
|
||
);
|
||
|
||
const filtered = repos.filter(
|
||
(r) =>
|
||
r.name.toLowerCase().includes(search.toLowerCase()) ||
|
||
r.full_name.toLowerCase().includes(search.toLowerCase())
|
||
);
|
||
|
||
async function handlePickDir() {
|
||
const dir = await openDialog({ directory: true, title: "选择本地存放目录" });
|
||
if (dir && selected) {
|
||
setLocalPath(`${dir}\\${selected.name}`.replace(/\\/g, "\\"));
|
||
} else if (dir) {
|
||
setLocalPath(dir as string);
|
||
}
|
||
}
|
||
|
||
async function handleImport() {
|
||
if (!selected || !localPath) return;
|
||
setCloning(true);
|
||
try {
|
||
const wsId = crypto.randomUUID();
|
||
await gitClone(selected.clone_url, localPath, upstreamUrl || undefined);
|
||
await createProject({
|
||
id: wsId,
|
||
name: selected.name,
|
||
description: selected.description ?? undefined,
|
||
win_path: localPath,
|
||
status: "active",
|
||
priority: "mid",
|
||
repo_url: selected.clone_url,
|
||
upstream_url: upstreamUrl || undefined,
|
||
default_branch: selected.default_branch,
|
||
space_id: currentSpace?.id,
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||
// 静默同步 catalog 到 .dev-spaces
|
||
(async () => {
|
||
try {
|
||
const account = await githubGetAccount();
|
||
const token = await githubGetToken();
|
||
if (token && account && currentSpace) {
|
||
await syncProjectCatalog(token, account.username, currentSpace.id, currentSpace.name, {
|
||
id: wsId,
|
||
name: selected.name,
|
||
description: selected.description ?? undefined,
|
||
repo_url: selected.clone_url,
|
||
default_branch: selected.default_branch,
|
||
platform: "windows",
|
||
});
|
||
}
|
||
} catch { /* 同步失败不影响导入 */ }
|
||
})();
|
||
showToast(`${selected.name} 已导入`);
|
||
onClose();
|
||
} catch (e) {
|
||
showToast(`导入失败: ${e}`);
|
||
} finally {
|
||
setCloning(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||
<div className="bg-zinc-900 rounded-xl shadow-2xl w-[620px] max-h-[80vh] flex flex-col border border-zinc-700">
|
||
{/* 标题 */}
|
||
<div className="px-5 py-4 border-b border-zinc-700 flex items-center justify-between">
|
||
<h2 className="text-base font-semibold text-white">从 GitHub 导入仓库</h2>
|
||
<button onClick={onClose} className="text-zinc-400 hover:text-white text-xl leading-none">×</button>
|
||
</div>
|
||
|
||
{isLoading && (
|
||
<div className="flex-1 flex items-center justify-center text-zinc-400 text-sm">
|
||
加载仓库列表...
|
||
</div>
|
||
)}
|
||
|
||
{error && (
|
||
<div className="flex-1 flex items-center justify-center text-red-400 text-sm p-6">
|
||
{String(error)}
|
||
</div>
|
||
)}
|
||
|
||
{!isLoading && !error && (
|
||
<>
|
||
{/* 搜索 */}
|
||
<div className="px-4 py-3 border-b border-zinc-800">
|
||
<input
|
||
type="text"
|
||
placeholder="搜索仓库..."
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
className="w-full bg-zinc-800 text-white text-sm rounded-lg px-3 py-2 outline-none focus:ring-1 focus:ring-blue-500 placeholder:text-zinc-500"
|
||
/>
|
||
</div>
|
||
|
||
{/* 仓库列表 */}
|
||
<div className="flex-1 overflow-y-auto min-h-0">
|
||
{filtered.map((repo) => {
|
||
const alreadyImported = importedUrls.has(repo.clone_url);
|
||
return (
|
||
<button
|
||
key={repo.id}
|
||
onClick={() => !alreadyImported && setSelected(repo)}
|
||
disabled={alreadyImported}
|
||
className={`w-full text-left px-4 py-3 border-b border-zinc-800 transition-colors ${
|
||
alreadyImported
|
||
? "opacity-40 cursor-not-allowed"
|
||
: selected?.id === repo.id
|
||
? "bg-zinc-800 border-l-2 border-l-blue-500"
|
||
: "hover:bg-zinc-800"
|
||
}`}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-white text-sm font-medium">{repo.name}</span>
|
||
{repo.private && (
|
||
<span className="text-xs px-1.5 py-0.5 bg-zinc-700 text-zinc-300 rounded">私有</span>
|
||
)}
|
||
{alreadyImported && (
|
||
<span className="text-xs px-1.5 py-0.5 bg-green-900 text-green-400 rounded">已导入</span>
|
||
)}
|
||
<span className="text-zinc-500 text-xs ml-auto">{repo.default_branch}</span>
|
||
</div>
|
||
{repo.description && (
|
||
<p className="text-zinc-400 text-xs mt-0.5 truncate">{repo.description}</p>
|
||
)}
|
||
</button>
|
||
);
|
||
})}
|
||
{filtered.length === 0 && (
|
||
<div className="text-center text-zinc-500 text-sm py-8">没有找到匹配的仓库</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 选中后的配置 */}
|
||
{selected && (
|
||
<div className="px-4 py-4 border-t border-zinc-700 space-y-3">
|
||
<div>
|
||
<label className="text-xs text-zinc-400 mb-1 block">本地路径</label>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
value={localPath}
|
||
onChange={(e) => setLocalPath(e.target.value)}
|
||
placeholder="选择本地存放目录..."
|
||
className="flex-1 bg-zinc-800 text-white text-sm rounded-lg px-3 py-2 outline-none focus:ring-1 focus:ring-blue-500 placeholder:text-zinc-500"
|
||
/>
|
||
<button
|
||
onClick={handlePickDir}
|
||
className="px-3 py-2 bg-zinc-700 text-zinc-200 text-sm rounded-lg hover:bg-zinc-600 transition-colors"
|
||
>
|
||
浏览
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label className="text-xs text-zinc-400 mb-1 block">
|
||
公司上游仓库地址 <span className="text-zinc-600">(可选)</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={upstreamUrl}
|
||
onChange={(e) => setUpstreamUrl(e.target.value)}
|
||
placeholder="https://github.com/company/project.git"
|
||
className="w-full bg-zinc-800 text-white text-sm rounded-lg px-3 py-2 outline-none focus:ring-1 focus:ring-blue-500 placeholder:text-zinc-500"
|
||
/>
|
||
</div>
|
||
<div className="flex justify-end gap-2 pt-1">
|
||
<button
|
||
onClick={onClose}
|
||
className="px-4 py-2 bg-zinc-700 text-zinc-200 text-sm rounded-lg hover:bg-zinc-600 transition-colors"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
onClick={handleImport}
|
||
disabled={!localPath || cloning}
|
||
className="px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
{cloning ? "克隆中..." : `导入 ${selected.name}`}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|