"use client"; import { useEffect, useState, useCallback } from "react"; import { Modal, Form, Input, InputNumber, Select, Tabs, Button, Space, App, Spin, } from "antd"; import { PlusOutlined, DeleteOutlined } from "@ant-design/icons"; import { useDebouncedCallback } from "use-debounce"; import { createConfig, updateConfig } from "@/services/config"; import { getModrinthLoaders, getModrinthEnvironments, getCurseForgeMeta, } from "@/services/meta"; import type { Config } from "@/types"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const DEPENDENCY_TYPES = [ { label: "Required", value: "required" }, { label: "Optional", value: "optional" }, { label: "Incompatible", value: "incompatible" }, { label: "Embedded", value: "embedded" }, ]; const RELATION_TYPES = [ { label: "Embedded Library", value: "embeddedLibrary" }, { label: "Incompatible", value: "incompatible" }, { label: "Optional Dependency", value: "optionalDependency" }, { label: "Required Dependency", value: "requiredDependency" }, { label: "Tool", value: "tool" }, ]; type ConfigValues = Config; // --------------------------------------------------------------------------- // Outer component — Modal shell, meta loading, state // --------------------------------------------------------------------------- interface ConfigModalProps { open: boolean; mode: "add" | "edit"; initialName: string | null; initialConfig: Config | null; onCancel: () => void; onSaved: (file: string, name: string) => void; } export function ConfigModal({ open, mode, initialName, initialConfig, onCancel, onSaved, }: ConfigModalProps) { const { message } = App.useApp(); const [saving, setSaving] = useState(false); // --------------- Metadata --------------- const [metaLoading, setMetaLoading] = useState(false); const [meta, setMeta] = useState<{ modrinthLoaders: string[]; modrinthEnvironments: { value: string; label: string }[]; curseforgeEnvs: string[]; curseforgeLoaders: string[]; } | null>(null); const loadMeta = useCallback(async () => { setMetaLoading(true); try { const [loaders, envs, cfMeta] = await Promise.all([ getModrinthLoaders(), getModrinthEnvironments(), getCurseForgeMeta(), ]); setMeta({ modrinthLoaders: loaders, modrinthEnvironments: envs, curseforgeEnvs: cfMeta.environments, curseforgeLoaders: cfMeta.loaders, }); } catch { message.warning("部分元数据加载失败,手动输入仍然可用"); } finally { setMetaLoading(false); } }, []); useEffect(() => { if (open) loadMeta(); }, [open, loadMeta]); // --------------- Save --------------- const handleSave = useCallback( async (values: ConfigValues) => { const data: Config = { ...values, modrinth: { ...values.modrinth, dependencies: values.modrinth.dependencies ?? [], }, curseforge: { ...values.curseforge, relations: values.curseforge.relations ?? { projects: [] }, }, }; setSaving(true); try { if (mode === "add") { await createConfig(data); onSaved(`${data.name}.json`, data.name); message.success("配置已创建"); } else { const oldName = (initialName ?? "").replace(/\.json$/, ""); await updateConfig(oldName, data); onSaved(`${data.name}.json`, data.name); message.success("配置已更新"); } } catch (err) { message.error( err instanceof Error ? err.message : "保存配置失败", ); } finally { setSaving(false); } }, [mode, initialName, onSaved], ); // --------------- Debounced project name lookup --------------- const [modrinthProjectName, setModrinthProjectName] = useState(null); const [curseforgeProjectName, setCurseforgeProjectName] = useState(null); const lookupModrinthProject = useDebouncedCallback(async (projectId: string) => { if (!projectId) { setModrinthProjectName(null); return; } // TODO: call Modrinth API to get project name setModrinthProjectName(null); }, 800); const lookupCurseforgeProject = useDebouncedCallback(async (projectId: number | null) => { if (projectId == null) { setCurseforgeProjectName(null); return; } // TODO: call CurseForge API to get project name setCurseforgeProjectName(null); }, 800); // --------------- Render --------------- return ( } > {metaLoading ? (
) : meta ? ( ) : null}
); } // --------------------------------------------------------------------------- // Inner component — Form (only rendered when meta is loaded) // --------------------------------------------------------------------------- interface ConfigModalFormProps { mode: "add" | "edit"; initialName: string | null; initialConfig: Config | null; meta: { modrinthLoaders: string[]; modrinthEnvironments: { value: string; label: string }[]; curseforgeEnvs: string[]; curseforgeLoaders: string[]; }; modrinthProjectName: string | null; curseforgeProjectName: string | null; lookupModrinthProject: (projectId: string) => void; lookupCurseforgeProject: (projectId: number | null) => void; onSave: (values: ConfigValues) => void; } function ConfigModalForm({ mode, initialConfig, meta, modrinthProjectName, curseforgeProjectName, lookupModrinthProject, lookupCurseforgeProject, onSave, }: ConfigModalFormProps) { const [form] = Form.useForm(); // Reset form when modal opens or mode/config changes useEffect(() => { if (mode === "edit" && initialConfig) { form.setFieldsValue(initialConfig); } else { form.resetFields(); form.setFieldsValue({ minecraft_properties_dir: "./properties", project_properties_path: "./gradle.properties", mod_version_field: "mod_version", filename_format: "", source_filename_format: "", modrinth: { project_id: "", version_name: "ModName v${version} for Minecraft ${mc_version_range}", version: "${version}-mc${mc_version}", loaders: [], environment: "client_and_server", dependencies: [], } as Config["modrinth"], curseforge: { project_id: 0, version_name: "#{filename_format}", environment: [], loaders: [], relations: { projects: [] }, } as Config["curseforge"], } as Partial); } }, [mode, initialConfig, form]); const handleFinish = useCallback( async (values: ConfigValues) => { onSave(values); }, [onSave], ); const handleFinishFailed = useCallback(() => { // Ant Design shows inline validation errors }, []); return (
{/* Hidden submit button triggered by modal footer */} )} ), }, // ──── CurseForge ──── { key: "curseforge", label: "CurseForge", children: ( <> lookupCurseforgeProject( typeof val === "number" ? val : null, ) } /> ({ label: l, value: l, }))} /> {/* Relations */} {(fields, { add, remove }) => ( <> {fields.map(({ key, name, ...rest }) => (