"use client"; import { useEffect, useState, useCallback } from "react"; import { Modal, Form, Input, InputNumber, Select, Tabs, Button, Space, App, } 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, DependencyType, UploadRelationType } from "@/types"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const DEPENDENCY_TYPE_LABEL_MAP: Record = { required: "Required", optional: "Optional", incompatible: "Incompatible", embedded: "Embedded" }; const DEPENDENCY_TYPES = Object.entries(DEPENDENCY_TYPE_LABEL_MAP).map(v => ({ value: v[0], label: v[1] })); const RELATION_TYPE_LABEL_MAP: Record = { incompatible: "Incompatible", embeddedLibrary: "Embedded Library", optionalDependency: "Optional Dependency", requiredDependency: "Required Dependency", tool: "Tool" }; const RELATION_TYPES = Object.entries(RELATION_TYPE_LABEL_MAP).map(v => ({ value: v[0], label: v[1] })); // --------------------------------------------------------------------------- // Data-fetching hooks — each Select loads independently, no blocking // --------------------------------------------------------------------------- function useModrinthLoaders(): { loading: boolean; options: { label: string; value: string }[]; } { const [loading, setLoading] = useState(true); const [options, setOptions] = useState<{ label: string; value: string }[]>( [], ); useEffect(() => { let cancelled = false; setLoading(true); getModrinthLoaders() .then((list) => { if (!cancelled) { setOptions(list.map((l) => ({ label: l, value: l }))); } }) .catch(() => {}) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, []); return { loading, options }; } function useModrinthEnvironments(): { loading: boolean; options: { value: string; label: string }[]; } { const [loading, setLoading] = useState(true); const [options, setOptions] = useState< { value: string; label: string }[] >([]); useEffect(() => { let cancelled = false; setLoading(true); getModrinthEnvironments() .then((list) => { if (!cancelled) setOptions(list); }) .catch(() => {}) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, []); return { loading, options }; } function useCurseforgeMeta(): { loading: boolean; environments: string[]; loaders: string[]; } { const [loading, setLoading] = useState(true); const [environments, setEnvironments] = useState([]); const [loaders, setLoaders] = useState([]); useEffect(() => { let cancelled = false; setLoading(true); getCurseForgeMeta() .then((meta) => { if (!cancelled) { setEnvironments(meta.environments); setLoaders(meta.loaders); } }) .catch(() => {}) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, []); return { loading, environments, loaders }; } type ConfigValues = Config; // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- 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); // --------------- 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, message], ); // --------------- 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 ( } > ); } // --------------------------------------------------------------------------- // Inner component — Form with independently-loading Selects // --------------------------------------------------------------------------- interface ConfigModalFormProps { mode: "add" | "edit"; initialConfig: Config | null; modrinthProjectName: string | null; curseforgeProjectName: string | null; lookupModrinthProject: (projectId: string) => void; lookupCurseforgeProject: (projectId: number | null) => void; onSave: (values: ConfigValues) => void; } function ConfigModalForm({ mode, initialConfig, modrinthProjectName, curseforgeProjectName, lookupModrinthProject, lookupCurseforgeProject, onSave, }: ConfigModalFormProps) { const [form] = Form.useForm(); // Each Select loads its own data independently const mrLoaders = useModrinthLoaders(); const mrEnvs = useModrinthEnvironments(); const cfMeta = useCurseforgeMeta(); // 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( (values: ConfigValues) => { onSave(values); }, [onSave], ); const handleFinishFailed = useCallback(() => {}, []); return (
)} ), }, // ──── CurseForge ──── { key: "curseforge", label: "CurseForge", children: ( <> lookupCurseforgeProject( typeof val === "number" ? val : null, ) } /> ({ label: l, value: l, }))} notFoundContent={ cfMeta.loading ? "加载中…" : "无可用选项" } /> {/* Relations */} {(fields, { add, remove }) => ( <> {fields.map(({ key, name, ...rest }) => (