dev-manager-tauri/src/components/dashboard/DeployModal.tsx
lanrtop 0baa304fbe feat: WSL路径支持、项目发布到GitHub、三灯状态指示
- 新增发布到GitHub功能(scan/init/commit/push/register全流程)
- 新增WSL发行版检测与UNC路径自动转换,路径存储改为Windows可访问格式
- 修复libgit2访问WSL UNC路径的owner验证问题(启动时全局禁用)
- ProjectCard新增本地项目/本地仓库/远程仓库三灯状态指示
- ProjectModal重构为基本信息+部署信息双标签页,固定高度稳定切换
- 添加项目支持顶部快速选择文件夹并自动填充ID和名称
- 设置页WSL区块改为下拉检测选择发行版
- Dialog组件支持fixedHeight固定高度模式
2026-04-01 01:36:21 +09:00

176 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
getDeployCommands,
addDeployCommand,
removeDeployCommand,
executeDeploy,
type DeployCommand,
} from "../../lib/commands";
import { useUIStore } from "../../store/ui";
interface Props {
projectId: string;
projectName: string;
projectPath?: string; // Windows 路径(win_path)
projectWslPath?: string; // WSL 路径(wsl_path),作为 WSL 命令的 fallback cwd
onClose: () => void;
}
export function DeployModal({ projectId, projectName, projectPath, projectWslPath, onClose }: Props) {
const queryClient = useQueryClient();
const showToast = useUIStore((s) => s.showToast);
const [newName, setNewName] = useState("");
const [newCmd, setNewCmd] = useState("");
const [newRunIn, setNewRunIn] = useState<"windows" | "wsl">("windows");
// 正在执行的命令 id
const [runningId, setRunningId] = useState<string | null>(null);
// 执行输出
const [output, setOutput] = useState<{ id: string; text: string; ok: boolean } | null>(null);
const { data: commands = [] } = useQuery({
queryKey: ["deployCommands", projectId],
queryFn: () => getDeployCommands(projectId),
});
const addMutation = useMutation({
mutationFn: () => addDeployCommand(projectId, newName, newCmd, newRunIn),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["deployCommands", projectId] });
setNewName("");
setNewCmd("");
},
onError: (e) => showToast(`添加失败: ${e}`),
});
const removeMutation = useMutation({
mutationFn: (id: string) => removeDeployCommand(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["deployCommands", projectId] }),
});
async function handleRun(cmd: DeployCommand) {
setRunningId(cmd.id);
setOutput(null);
try {
// WSL 命令优先用 projectPath(Windows 路径,WSL 自动映射 /mnt/...)
// 若无 win_path,则将 wsl_path 通过 `cd && ` 前缀传入命令
let effectiveCommand = cmd.command;
let cwd: string | undefined = projectPath;
if (cmd.run_in === "wsl" && !projectPath && projectWslPath) {
effectiveCommand = `cd ${JSON.stringify(projectWslPath)} && ${cmd.command}`;
cwd = undefined;
}
const result = await executeDeploy(effectiveCommand, cmd.run_in, cwd);
setOutput({ id: cmd.id, text: result, ok: true });
} catch (e) {
setOutput({ id: cmd.id, text: String(e), ok: false });
} finally {
setRunningId(null);
}
}
const canAdd = newName.trim() && newCmd.trim();
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-[580px] 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">
<div>
<h2 className="text-base font-semibold text-white">部署命令</h2>
<p className="text-xs text-zinc-400 mt-0.5">{projectName}</p>
</div>
<button onClick={onClose} className="text-zinc-400 hover:text-white text-xl leading-none">×</button>
</div>
<div className="flex-1 overflow-y-auto min-h-0 p-4 space-y-3">
{/* 已有命令列表 */}
{commands.length === 0 && (
<p className="text-zinc-500 text-sm text-center py-4">暂无部署命令</p>
)}
{commands.map((cmd) => (
<div key={cmd.id} className="bg-zinc-800 rounded-lg p-3 space-y-2">
<div className="flex items-center gap-2">
<span className="text-white text-sm font-medium flex-1">{cmd.name}</span>
<span className={`text-xs px-1.5 py-0.5 rounded font-mono ${
cmd.run_in === "wsl"
? "bg-green-900 text-green-300"
: "bg-blue-900 text-blue-300"
}`}>
{cmd.run_in.toUpperCase()}
</span>
<button
onClick={() => handleRun(cmd)}
disabled={runningId === cmd.id}
className="px-3 py-1 bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white text-xs rounded transition-colors"
>
{runningId === cmd.id ? "执行中…" : "▶ 执行"}
</button>
<button
onClick={() => removeMutation.mutate(cmd.id)}
className="text-zinc-500 hover:text-red-400 text-sm transition-colors"
title="删除"
>
✕
</button>
</div>
<code className="block text-xs text-zinc-400 font-mono bg-zinc-900 rounded px-2 py-1.5 break-all">
{cmd.command}
</code>
{/* 执行输出 */}
{output?.id === cmd.id && (
<div className={`rounded px-2 py-1.5 text-xs font-mono whitespace-pre-wrap break-all max-h-32 overflow-y-auto ${
output.ok ? "bg-zinc-950 text-green-400" : "bg-red-950 text-red-300"
}`}>
{output.text}
</div>
)}
</div>
))}
</div>
{/* 添加新命令 */}
<div className="px-4 py-4 border-t border-zinc-700 space-y-2">
<p className="text-xs text-zinc-400 font-medium">添加命令</p>
<div className="flex gap-2">
<input
type="text"
placeholder="名称(如:启动服务)"
value={newName}
onChange={(e) => setNewName(e.target.value)}
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-600"
/>
<select
value={newRunIn}
onChange={(e) => setNewRunIn(e.target.value as "windows" | "wsl")}
className="bg-zinc-800 text-white text-sm rounded-lg px-2 py-2 outline-none focus:ring-1 focus:ring-blue-500"
>
<option value="windows">Windows</option>
<option value="wsl">WSL</option>
</select>
</div>
<div className="flex gap-2">
<input
type="text"
placeholder="命令(如:docker compose up -d)"
value={newCmd}
onChange={(e) => setNewCmd(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && canAdd && addMutation.mutate()}
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-600 font-mono"
/>
<button
onClick={() => addMutation.mutate()}
disabled={!canAdd || addMutation.isPending}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white text-sm rounded-lg transition-colors"
>
添加
</button>
</div>
</div>
</div>
</div>
);
}