- 新增发布到GitHub功能(scan/init/commit/push/register全流程) - 新增WSL发行版检测与UNC路径自动转换,路径存储改为Windows可访问格式 - 修复libgit2访问WSL UNC路径的owner验证问题(启动时全局禁用) - ProjectCard新增本地项目/本地仓库/远程仓库三灯状态指示 - ProjectModal重构为基本信息+部署信息双标签页,固定高度稳定切换 - 添加项目支持顶部快速选择文件夹并自动填充ID和名称 - 设置页WSL区块改为下拉检测选择发行版 - Dialog组件支持fixedHeight固定高度模式
176 lines
7.1 KiB
TypeScript
176 lines
7.1 KiB
TypeScript
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>
|
||
);
|
||
}
|