import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { getProjectServerLinks, getServers, linkServer, unlinkServer, type ProjectServerLink, type QuickLink, } from "../../lib/commands"; function CopyBtn({ value }: { value: string }) { const [copied, setCopied] = useState(false); const handleCopy = async () => { await navigator.clipboard.writeText(value); setCopied(true); setTimeout(() => setCopied(false), 1500); }; return ( ); } function buildSshCmd(link: ProjectServerLink): string { const port = link.server_port && link.server_port !== "22" ? ` -p ${link.server_port}` : ""; const key = link.ssh_key_path ? ` -i ${link.ssh_key_path}` : ""; const user = link.username ? `${link.username}@` : ""; return `ssh ${user}${link.server_host}${port}${key}`; } function buildDeployKeyCmd(link: ProjectServerLink): string { const keyPub = link.ssh_key_path ? link.ssh_key_path.replace(/(?:\.pub)?$/, ".pub") : "%USERPROFILE%\\.ssh\\id_ed25519.pub"; const portFlag = link.server_port && link.server_port !== "22" ? ` -p ${link.server_port}` : ""; const user = link.username ? `${link.username}@` : ""; return `type ${keyPub} | ssh${portFlag} ${user}${link.server_host} "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"`; } // ── 已关联服务器条目 ──────────────────────────────────────────────────────── function LinkedServerItem({ link, projectId }: { link: ProjectServerLink; projectId: string }) { const qc = useQueryClient(); const [showPwd, setShowPwd] = useState(false); const unlinkMut = useMutation({ mutationFn: () => unlinkServer(projectId, link.server_id), onSuccess: () => { qc.invalidateQueries({ queryKey: ["project-servers", projectId] }); qc.invalidateQueries({ queryKey: ["server-projects", link.server_id] }); }, }); const sshCmd = buildSshCmd(link); const links: QuickLink[] = (() => { try { return link.quick_links ? JSON.parse(link.quick_links) : []; } catch { return []; } })(); return (
{/* 标题行 */}
{link.server_name} {link.server_host} {link.role && ( {link.role} )}
{/* 信息行 */}
{link.deploy_type && (
{link.deploy_type === "docker" ? "Docker" : "裸机"}
)} {link.username && (
用户: {link.username}
)} {link.password && (
密码: {showPwd ? link.password : "••••"}
)} {link.deploy_path && (
{link.deploy_type === "docker" ? "Compose:" : "路径:"} {link.deploy_path}
)} {link.container_name && (
容器: {link.container_name}
)} {link.start_command && (
启动: {link.start_command}
)}
{/* SSH 命令 */}
$ {sshCmd}
{/* 部署公钥命令 */}
▶ 部署公钥(CMD)
> {buildDeployKeyCmd(link)}
{/* 快速链接 */} {links.length > 0 && (
{links.map((l, i) => (
{l.label || l.url}
))}
)}
); } // ── 关联选择器 ────────────────────────────────────────────────────────────── function LinkSelector({ projectId, linkedIds }: { projectId: string; linkedIds: Set }) { const qc = useQueryClient(); const [selectedServerId, setSelectedServerId] = useState(""); const [role, setRole] = useState("production"); const [deployType, setDeployType] = useState<"bare" | "docker">("bare"); const [deployPath, setDeployPath] = useState(""); const [containerName, setContainerName] = useState(""); const [startCommand, setStartCommand] = useState(""); const { data: allServers = [] } = useQuery({ queryKey: ["servers"], queryFn: getServers, staleTime: 30000, }); const available = allServers.filter((s) => !linkedIds.has(s.id)); const linkMut = useMutation({ mutationFn: () => linkServer(projectId, selectedServerId, { role: role || undefined, deploy_type: deployType, deploy_path: deployPath || undefined, container_name: containerName || undefined, start_command: startCommand || undefined, }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["project-servers", projectId] }); qc.invalidateQueries({ queryKey: ["server-projects", selectedServerId] }); setSelectedServerId(""); setRole("production"); setDeployType("bare"); setDeployPath(""); setContainerName(""); setStartCommand(""); }, }); if (available.length === 0) { return (

{allServers.length === 0 ? "请先在「服务器」Tab 中添加服务器" : "所有服务器已关联"}

); } const inputCls = "text-xs px-2.5 py-2 rounded-md border border-gray-200 bg-white focus:outline-none focus:border-blue-300 focus:ring-1 focus:ring-blue-100"; return (
{/* 第一行:服务器 + 角色 + 部署方式 */}
{/* 第二行:根据部署类型显示不同字段 */}
{deployType === "bare" ? ( <>
setDeployPath(e.target.value)} placeholder="/var/www/app" className={inputCls} />
setStartCommand(e.target.value)} placeholder="pm2 restart app" className={inputCls} />
) : ( <>
setContainerName(e.target.value)} placeholder="my-app" className={inputCls} />
setDeployPath(e.target.value)} placeholder="/opt/docker/compose.yml" className={inputCls} />
)}
{/* Docker 额外行:启动命令 */} {deployType === "docker" && (
setStartCommand(e.target.value)} placeholder="docker compose up -d" className={inputCls} />
)}
); } // ── 主面板(嵌入 ProjectCard) ────────────────────────────────────────────── export function ProjectServerPanel({ projectId }: { projectId: string }) { const [showLink, setShowLink] = useState(false); const { data: links = [] } = useQuery({ queryKey: ["project-servers", projectId], queryFn: () => getProjectServerLinks(projectId), }); const linkedIds = new Set(links.map((l) => l.server_id)); return (
关联服务器
{showLink && } {links.length === 0 && !showLink && (

暂未关联服务器

)} {links.map((link) => ( ))}
); }