From 7173ef11494241f85a68a3fb32d918d371a920b9 Mon Sep 17 00:00:00 2001 From: CPTProgrammer <46586216+CPTProgrammer@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:18:20 +0800 Subject: [PATCH] Refactor environment selector with grouped options and descriptions --- src/components/ConfigModal.tsx | 38 +++++++--- src/services/meta.ts | 132 ++++++++++++++++++++++++++++----- 2 files changed, 140 insertions(+), 30 deletions(-) diff --git a/src/components/ConfigModal.tsx b/src/components/ConfigModal.tsx index 8ba9cd5..c4e5db9 100644 --- a/src/components/ConfigModal.tsx +++ b/src/components/ConfigModal.tsx @@ -20,6 +20,8 @@ import { getModrinthLoaders, getModrinthEnvironments, getCurseForgeMeta, + type EnvironmentGroup, + EnvironmentOption, } from "@/services/meta"; import { scanMcPropertiesDir, @@ -327,12 +329,10 @@ function useModrinthLoaders(): { function useModrinthEnvironments(): { loading: boolean; - options: { value: string; label: string }[]; + options: EnvironmentGroup[]; } { const [loading, setLoading] = useState(true); - const [options, setOptions] = useState< - { value: string; label: string }[] - >([]); + const [options, setOptions] = useState([]); useEffect(() => { let cancelled = false; @@ -507,8 +507,8 @@ function ConfigModalForm({ const { token } = theme.useToken(); // Each Select loads its own data independently - const mrLoaders = useModrinthLoaders(); const mrEnvs = useModrinthEnvironments(); + const mrLoaders = useModrinthLoaders(); const cfMeta = useCurseforgeMeta(); const sortedMrOptions = useMemo(() => { @@ -551,22 +551,19 @@ function ConfigModalForm({ filename_format: "modname-${version}-mc${mc_version}.jar", source_filename_format: "modname-${version}-mc${mc_version}-sources.jar", 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"], + } as Partial, curseforge: { - project_id: 0, version_name: "#{filename_format}", environment: [], loaders: [], relations: { projects: [] }, - } as Config["curseforge"], - } as Partial); + } as Partial, + }); } }, [mode, initialConfig, form]); @@ -755,6 +752,25 @@ function ConfigModalForm({ placeholder="选择运行环境" loading={mrEnvs.loading} options={mrEnvs.options} + optionRender={(option) => { + const desc = (option.data as unknown as EnvironmentOption).description; + return ( +
+
{option.label}
+ {desc && ( +
+ {desc} +
+ )} +
+ ); + }} notFoundContent={ mrEnvs.loading ? "加载中…" : "无可用选项" } diff --git a/src/services/meta.ts b/src/services/meta.ts index 7be5370..40dda16 100644 --- a/src/services/meta.ts +++ b/src/services/meta.ts @@ -4,21 +4,118 @@ import { modrinthClient, curseforgeClient } from "./clients"; import { getLoaders, getGameVersions as getModrinthGameVersions } from "@/lib/modrinth"; import { getVersionTypes, getGameVersions, getMinecraftVersions } from "@/lib/curseforge"; import { SemVer } from "@/lib/utils/templates/semver"; -import { Environment, EnvironmentSchema } from "@/types"; +import { Environment } from "@/types"; -const ENVIRONMENT_LABELS: Record = { - unknown: "Unknown environment", - client_only: "Client-side only", - server_only: "Server-side only (singleplayer compatible)", - dedicated_server_only: "Dedicated server only", - client_and_server: "Required on both", - server_only_client_optional: "Optional on client", - client_only_server_optional: "Optional on server", - client_or_server_prefers_both: "Optional on both (prefers both)", - client_or_server: "Optional on both (either side)", - singleplayer_only: "Singleplayer only", +const ENVIRONMENT_DESCRIPTIONS: Record = { + client_only: + "All functionality is done client-side and is compatible with vanilla servers.", + server_only: + "All functionality is done server-side and is compatible with vanilla clients. Also works on the internal server in singleplayer.", + dedicated_server_only: + "All functionality is done server-side and is compatible with vanilla clients. Only works on dedicated servers.", + client_and_server: "Required on both the client and server.", + server_only_client_optional: + "Most functionality is server-side, but installing it on the client enables enhanced functionality.", + client_only_server_optional: + "Most functionality is client-side, but installing it on the server enables enhanced functionality.", + client_or_server_prefers_both: + "Has some functionality on both the client and server, even if only partially. Installing it on both leads to the best experience.", + client_or_server: + "Has some functionality on both the client and server, even if only partially.", + singleplayer_only: + "Only functions in Singleplayer or when not connected to a Multiplayer server.", + unknown: "The environment for this version could not be determined.", }; +export interface EnvironmentOption { + value: Environment; + label: string; + description: string; +} + +export interface EnvironmentGroup { + label: string; + options: EnvironmentOption[]; +} + +const ENVIRONMENT_GROUPS: EnvironmentGroup[] = [ + { + label: "Client-side only", + options: [ + { + value: "client_only", + label: "Client-side only", + description: ENVIRONMENT_DESCRIPTIONS.client_only, + }, + ], + }, + { + label: "Server-side only", + options: [ + { + value: "server_only", + label: "Works in singleplayer too", + description: ENVIRONMENT_DESCRIPTIONS.server_only, + }, + { + value: "dedicated_server_only", + label: "Dedicated server only", + description: ENVIRONMENT_DESCRIPTIONS.dedicated_server_only, + }, + ], + }, + { + label: "Client and server", + options: [ + { + value: "client_and_server", + label: "Required on both", + description: ENVIRONMENT_DESCRIPTIONS.client_and_server, + }, + { + value: "server_only_client_optional", + label: "Optional on client", + description: ENVIRONMENT_DESCRIPTIONS.server_only_client_optional, + }, + { + value: "client_only_server_optional", + label: "Optional on server", + description: ENVIRONMENT_DESCRIPTIONS.client_only_server_optional, + }, + { + value: "client_or_server_prefers_both", + label: "Optional on both, works best when installed on both sides", + description: ENVIRONMENT_DESCRIPTIONS.client_or_server_prefers_both, + }, + { + value: "client_or_server", + label: "Optional on both, works the same if installed on either side", + description: ENVIRONMENT_DESCRIPTIONS.client_or_server, + }, + ], + }, + { + label: "Singleplayer only", + options: [ + { + value: "singleplayer_only", + label: "Singleplayer only", + description: ENVIRONMENT_DESCRIPTIONS.singleplayer_only, + }, + ], + }, + { + label: "Other", + options: [ + { + value: "unknown", + label: "Unknown environment", + description: ENVIRONMENT_DESCRIPTIONS.unknown, + }, + ], + }, +]; + /** * 获取 Modrinth 支持的加载器名称列表。 * 用于 ConfigModal 中 loaders 多选下拉框的选项。 @@ -40,14 +137,11 @@ export async function getModrinthMcVersions(): Promise { } /** - * 获取 Modrinth 运行环境枚举值,转为 Ant Design Select 的选项格式。 - * 前端使用 OptGroup 分组显示(Client / Server / Both / Other)。 + * 获取 Modrinth 运行环境枚举值,按 Ant Design OptGroup 分组。 + * 分组来源于 Modrinth 前端 UI 的 environments.ts,英文描述也完全一致。 */ -export async function getModrinthEnvironments() { - return EnvironmentSchema.options.map((value) => ({ - value, - label: ENVIRONMENT_LABELS[value], - })); +export async function getModrinthEnvironments(): Promise { + return ENVIRONMENT_GROUPS; } /**