import { CheckCircle2, CircleDashed, Compass, FolderCog, Rocket, ServerCog } from "lucide-react"; import { type ChangeEvent, type FormEvent, useEffect, useMemo, useState } from "react"; import type { GamePluginResponse, RunEndpointResponse, ServerDeploymentResponse } from "../api/types"; import { ManagementDialog } from "./OperationControls"; import { endpointLabel, pluginCreateInputDefaults, pluginLabel, runtimeBindingFields, type ServerCreateFormState } from "../contracts/serverManagement"; import { cx } from "../utils/classes"; type WorkflowKind = "create" | "edit"; interface ServerDeploymentWorkflowProps { open: boolean; kind: WorkflowKind; plugins: GamePluginResponse[]; endpoints: RunEndpointResponse[]; initialForm: ServerCreateFormState; deployment?: ServerDeploymentResponse; busy?: boolean; onClose: () => void; onSubmit: (form: ServerCreateFormState, saveAsDraft: boolean) => Promise; } const workflowSteps = [ { label: "选择目标", icon: Compass }, { label: "部署方式", icon: ServerCog }, { label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket } ]; export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initialForm, deployment, busy = false, onClose, onSubmit }: ServerDeploymentWorkflowProps) { const [step, setStep] = useState(0); const [form, setForm] = useState(initialForm); const [saveAsDraft, setSaveAsDraft] = useState(false); const selectedPlugin = useMemo(() => plugins.find((plugin) => plugin.id === form.pluginId), [form.pluginId, plugins]); const profileOptions = selectedPlugin?.runtimeProfiles?.lifecycleProfiles ?? []; const pluginFields = selectedPlugin?.createFields ?? []; const bindingFields = runtimeBindingFields(selectedPlugin, form.profileKey); const activeServer = kind === "edit" && Boolean(deployment); useEffect(() => { if (!open) return; setStep(0); setSaveAsDraft(!initialForm.runEndpointId); setForm(initialForm); }, [initialForm, kind, open]); function updateForm(event: ChangeEvent) { const { name, value } = event.target; setForm((current) => { if (name === "pluginId") { const plugin = plugins.find((item) => item.id === value); return { ...current, pluginId: value, profileKey: plugin?.runtimeProfiles?.lifecycleProfiles?.[0]?.key ?? "", bindings: {}, createInputs: pluginCreateInputDefaults(plugin) }; } if (name === "profileKey") return { ...current, profileKey: value, bindings: {} }; return { ...current, [name]: value }; }); } function updateCreateInput(key: string, value: string) { setForm((current) => ({ ...current, createInputs: { ...current.createInputs, [key]: value } })); } function updateBinding(key: string, value: string) { setForm((current) => ({ ...current, bindings: { ...current.bindings, [key]: value } })); } function canContinue() { if (step === 0) return Boolean(form.pluginId && (saveAsDraft || form.runEndpointId)); if (step === 2) { if (kind === "create" && !form.name.trim()) return false; if (form.deploymentMode === "custom-command" && !form.startCommand.trim() && !deployment?.startCommandConfigured) return false; return pluginFields.filter((field) => field.required).every((field) => Boolean(form.createInputs[field.key]?.trim())); } return true; } async function submit(event: FormEvent) { event.preventDefault(); if (step < workflowSteps.length - 1) { if (canContinue()) setStep((current) => current + 1); return; } await onSubmit({ ...form, runEndpointId: saveAsDraft ? "" : form.runEndpointId }, saveAsDraft); } const protectedState = (nextValue: string, configured: boolean) => nextValue.trim() ? "将替换" : configured ? "保持已配置" : "未配置"; const actionLabel = kind === "create" ? (saveAsDraft ? "保存草稿" : "创建并部署") : (saveAsDraft ? "保存草稿设置" : "保存部署设置"); return { if (!busy) onClose(); }}>
void submit(event)} aria-label={kind === "create" ? "创建服务器部署向导" : "编辑服务器部署向导"}>
    {workflowSteps.map((item, index) => { const Icon = item.icon; return
  1. {index < step ? : }{index + 1}. {item.label}
  2. ; })}
{step === 0 &&
先确定游戏插件决定可填写的游戏配置与启动预设。
再选择节点节点负责预检和执行,平台不会直接访问主机。
也可存为草稿草稿不会发起部署,稍后可从服务器详情继续。
} {step === 1 &&

选择你想让平台承担的方式。后续只显示这一方式需要的字段。

setForm((current) => ({ ...current, deploymentMode: "guided-install" }))} /> setForm((current) => ({ ...current, deploymentMode: "existing-server" }))} /> setForm((current) => ({ ...current, deploymentMode: "custom-command" }))} />
} {step === 2 &&
{kind === "create" && } {kind === "create" && } {pluginFields.map((field) => ( ))}
{form.deploymentMode === "custom-command" &&
高级启动设置

只有自定义启动器需要这些设置。执行目录留空时,节点以服务器目录执行。

} {kind === "create" && bindingFields.length > 0 &&
运行连接设置

用于插件声明的逻辑连接,不是服务器目录或游戏配置。

{bindingFields.map((field) => )}
}
} {step === 3 &&
插件类型{pluginLabel(selectedPlugin, form.pluginId)}
目标{saveAsDraft ? "保存为未绑定草稿" : endpointLabel(endpoints.find((endpoint) => endpoint.id === form.runEndpointId), form.runEndpointId)}
部署方式{form.deploymentMode === "guided-install" ? "新建并安装" : form.deploymentMode === "existing-server" ? "接管已有服务器" : "自定义启动方式"}
服务器目录{protectedState(form.serverRoot, Boolean(deployment?.serverRootConfigured))}
{form.deploymentMode === "custom-command" && <>
启动命令{protectedState(form.startCommand, Boolean(deployment?.startCommandConfigured))}
执行目录{protectedState(form.workingDirectory, Boolean(deployment?.workingDirectoryConfigured))}
}
游戏配置{Object.keys(form.createInputs).length ? `${Object.keys(form.createInputs).length} 项已准备` : "使用插件默认值"}
{saveAsDraft ? "本次只保存定义" : activeServer ? "本次只保存部署设置" : "确认后将创建并派发部署"}{saveAsDraft ? "后续从服务器详情选择节点并部署。" : activeServer ? "保存后可在详情中明确发起部署;路径和命令不会显示原文。" : "Run 会在领取任务后执行本机预检,再进行安装、配置与启动。"}
}
{step < workflowSteps.length - 1 ? : }
; } function ModeOption({ active, title, copy, onClick }: { active: boolean; title: string; copy: string; onClick: () => void }) { return ; }